From 761174c08b1cceb0e9ccc37005bfca9f7958b25a Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 18 Mar 2020 16:10:11 +0800 Subject: [PATCH 001/213] add opengl notes --- ...71\345\271\225\345\256\236\347\216\260.md" | 2 + ...00\201MediaCodec\343\200\201MediaMuxer.md" | 0 .../1.OpenGL\347\256\200\344\273\213.md" | 254 +++ .../10.OpenGL ES\346\273\244\351\225\234.md" | 60 + ....GLSurfaceView\347\256\200\344\273\213.md" | 216 ++ ...20\347\240\201\350\247\243\346\236\220.md" | 690 ++++++ ....GLTextureView\345\256\236\347\216\260.md" | 1974 +++++++++++++++++ ...66\344\270\211\350\247\222\345\275\242.md" | 863 +++++++ ...42\345\217\212\345\234\206\345\275\242.md" | 511 +++++ ...45\231\250\350\257\255\350\250\200GLSL.md" | 232 ++ ...\261\273\345\217\212Matrix\347\261\273.md" | 143 ++ .../8.OpenGL ES\347\272\271\347\220\206.md" | 547 +++++ ...55\346\224\276\350\247\206\351\242\221.md" | 108 + .../OpenGL\347\256\200\344\273\213.md" | 52 - .../SurfaceView\344\270\216TextureView.md" | 2 +- .../\345\205\263\351\224\256\345\270\247.md" | 41 +- ...72\347\241\200\347\237\245\350\257\206.md" | 78 +- 17 files changed, 5717 insertions(+), 56 deletions(-) rename "VideoDevelopment/Android\345\274\271\345\271\225\345\256\236\347\216\260.md" => "VideoDevelopment/Danmaku/Android\345\274\271\345\271\225\345\256\236\347\216\260.md" (99%) rename "VideoDevelopment/GLSurfaceView\347\256\200\344\273\213.md" => "VideoDevelopment/MediaExtractor\343\200\201MediaCodec\343\200\201MediaMuxer.md" (100%) create mode 100644 "VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" create mode 100644 "VideoDevelopment/OpenGL/10.OpenGL ES\346\273\244\351\225\234.md" create mode 100644 "VideoDevelopment/OpenGL/2.GLSurfaceView\347\256\200\344\273\213.md" create mode 100644 "VideoDevelopment/OpenGL/3.GLSurfaceView\346\272\220\347\240\201\350\247\243\346\236\220.md" create mode 100644 "VideoDevelopment/OpenGL/4.GLTextureView\345\256\236\347\216\260.md" create mode 100644 "VideoDevelopment/OpenGL/4.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" create mode 100644 "VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" create mode 100644 "VideoDevelopment/OpenGL/6.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" create mode 100644 "VideoDevelopment/OpenGL/7.GLES\347\261\273\345\217\212Matrix\347\261\273.md" create mode 100644 "VideoDevelopment/OpenGL/8.OpenGL ES\347\272\271\347\220\206.md" create mode 100644 "VideoDevelopment/OpenGL/9.GLSurfaceView+MediaPlayer\346\222\255\346\224\276\350\247\206\351\242\221.md" delete mode 100644 "VideoDevelopment/OpenGL\347\256\200\344\273\213.md" diff --git "a/VideoDevelopment/Android\345\274\271\345\271\225\345\256\236\347\216\260.md" "b/VideoDevelopment/Danmaku/Android\345\274\271\345\271\225\345\256\236\347\216\260.md" similarity index 99% rename from "VideoDevelopment/Android\345\274\271\345\271\225\345\256\236\347\216\260.md" rename to "VideoDevelopment/Danmaku/Android\345\274\271\345\271\225\345\256\236\347\216\260.md" index 66136f9d..6a0ef80d 100644 --- "a/VideoDevelopment/Android\345\274\271\345\271\225\345\256\236\347\216\260.md" +++ "b/VideoDevelopment/Danmaku/Android\345\274\271\345\271\225\345\256\236\347\216\260.md" @@ -84,6 +84,8 @@ #### 自定义TextureView +#### OpenGL实现 + diff --git "a/VideoDevelopment/GLSurfaceView\347\256\200\344\273\213.md" "b/VideoDevelopment/MediaExtractor\343\200\201MediaCodec\343\200\201MediaMuxer.md" similarity index 100% rename from "VideoDevelopment/GLSurfaceView\347\256\200\344\273\213.md" rename to "VideoDevelopment/MediaExtractor\343\200\201MediaCodec\343\200\201MediaMuxer.md" 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" new file mode 100644 index 00000000..0c7f6f70 --- /dev/null +++ "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" @@ -0,0 +1,254 @@ +## 1.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 ES + +[OpenGL ES 官网](https://www.khronos.org/opengles/) + +OpenGL ES是免费的跨平台的功能完善的2D/3D图形库接口的API,是OpenGL的一个子集,主要针对手机、Pad和游戏主机等嵌入式设备而设计。 + +移动端使用到的基本上都是OpenGl ES,当然Android开发下还专门为OpenGl提供了android.opengl包,并且提供了GlSurfaceView,GLU,GlUtils等工具类。 + + + +### OpenGL的作用 + + + +手机上做图像处理用很多方法,但是目前为止最高效的方法就是有效的使用图形处理单元(GPU),图像的处理和渲染就是在将要渲染到窗口上的像素上做很多的浮点匀速,而GPU可以并行的做浮点运算,所以用GPU来分担CPU的部分,可以提高效率。 + +- 图片处理:比如图片色调转换、美颜等。 +- 摄像头预览效果处理。比如美颜相机、恶搞相机等。 +- 视频处理。视频播放的时候增加一些滤镜效果。 +- 3D游戏。比如神庙逃亡、都市赛车等。 + + + +### Android中的OpenGL ES + +Android中OpenGL ES的版本支持如下: + +- OpenGL ES 1.0 和 1.1 - 此 API 规范受 Android 1.0 及更高版本的支持。 +- OpenGL ES 2.0 - 此 API 规范受 Android 2.2(API 级别 8)及更高版本的支持。 +- OpenGL ES 3.0 - 此 API 规范受 Android 4.3(API 级别 18)及更高版本的支持。 +- OpenGL ES 3.1 - 此 API 规范受 Android 5.0(API 级别 21)及更高版本的支持。 + + + +### 渲染管线 + +OpenGL 1.x系列采用的是固定功能管线。 + +OpenGL ES 2.0开始采用了可编程图形管线。 + +OpenGL ES 3.0兼容了2.0,并丰富了更多功能。 + +OpenGL渲染管线的流程为: 顶点数据 -> 顶点着色器 -> 图元装配 -> 几何着色器 -> 光栅化 -> 片段着色器 -> 逐片段处理 -> 帧缓冲 + +如下图,阴影的表示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) + + + +- 顶点着色器(Vertex Shader): 用来渲染图形顶点的OpenGL ES代码。生成每个顶点的最终位置 + + 着色器(shader)是在GPU上运行的小程序。从名称可以看出,可通过处理它们来处理顶点。此程序使用OpenGL ES SL语言来编写。它是一个描述顶点或像素特性的简单程序。OpenGL最本质的概念之一就是着色器,它是图形硬件设备所执行的一类特殊函数。理解着色器最好的办法就是把它看做是专为图形处理单元(即GPU)编译的一种小型程序。任何一种OpenGL程序本质上都可以被分为两部分:CPU端运行的部分,采用C++、Java之类的语言编写;以及GPU端运行的部分,使用GLSL语言编写。 + + 顶点着色器可以操作的属性有: 位置、颜色、纹理坐标,但是不能创建新的顶点。最终产生纹理坐标、颜色、点位置等信息送往后续阶段。 + +- 图元装配(Primitive Assembly) + + 顶点组合成图元的过程叫做图元装配,这里的图元就是指点、线、三角。OpenGL ES中最基础且唯一的多边形就是三角形,所有更复杂的图形都是由三角形组成的。复杂的图形都可以拆分成多个三角形。比如OpenGL提供给开发者的绘制方法glDrawArrays,这个方法的第一个参数就是指定绘制方式,可选值有: + + - GL_POINTS:以点的形式绘制 + - GL_LINES:以线的形式绘制 + - GL_TRIANGLE_STRIP:以三角形的形式绘制,所有二维图像的渲染都会使用这种方式。 + + 该过程还有两个重要操作:裁剪和淘汰。 + + - 裁剪是指对于不在视椎体(屏幕上可见的3D区域)内的图元进行裁剪。 + + - 淘汰是指根据图元面向前方或后方选择排期它们(如事物内部的点)。 + +- 光栅化阶段(Rasterization Stage) + + 这里会把图元映射为最终屏幕上相应的像素,生成供片段着色器(fragment shader)使用的片段。在图元装配后传递过来的图元数据中,这些图元信息只是顶点而已。顶点处都还没有像素点,直线段端点之间是空的、多边形的边和内部也是空的,光栅化的任务就是构造这些。将图片转化为片段(fragment)的过程叫做光栅化。 这个阶段会将图元数据分解成更小的单元并对应于帧缓冲区的各个像素,这些单元成为片元,一个片元可能包含窗口颜色、纹理坐标等属性。 + + 光栅化其实是一种将几何图元变成二维图像的过程。在这里,虚拟3D世界中的物体投影到平面上,并生成一系列的片段。 + +- 片段着色器或片元着色器(Fragment Shader):使用颜色或纹理(texture)渲染图形表面的OpenGLES代码。 + + 主要目的是计算一个像素的最终颜色。光栅化操作构造了像素点,这个阶段就是处理这些像素点,根据自己的业务,例如高亮、饱和度调节、高斯模糊等来变化这个片元的颜色。为组成点、直线和三角形的每个片元生成最终颜色/纹理,针对每个片元都会执行一次,一个片元是一个小的、单一颜色的长方形区域,类似于计算机屏幕上的一个像素。一旦最终颜色生成,OpenGL就会把它们写到一块成为帧缓冲区的内存块中,然后Android就会把这个帧缓冲区显示到屏幕上。 + + 通常在这里对片段进行处理(纹理采样、颜色汇总等),将每个片段的颜色等属性计算出来并送给后续阶段。 + +- 逐片段操作 + + 具体的细分步骤又分为: + + ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_es_fragment_opera.jpg) + + - 像素归属测试 + + 判断当前像素是否归OpenGL所有,即OpenGLES帧缓冲区窗口的部分是否被另一个窗口所遮蔽,被遮挡像素不属于OpenGL上下文。 + + - 裁剪测试 + + 判断当前像素是否位于裁剪矩形范围内,如果位于裁剪区域外则被抛弃。 + + - 模板测试 + + 模板测试主要将绘制区域限定在一定范围内,一般用在湖面倒影、镜像等场合。 + + - 深度测试 + + 深度测试是将输入片元的深度与帧缓冲区中对应片元的深度进行比较,确定片段是否应该被拒绝。 + + - 混合 + + 将新生成的片段和保存在缓冲区的片段进行混合。 + + - 抖动 + + 用于最小化,因为使用有限精度在帧缓冲区中保存颜色值而产生的伪像,使用少量颜色模拟更宽的颜色范围。 + +- 帧缓冲区 + + OpenGL管线的最终渲染目的地被称为帧缓存(framebuffer也被记做FBO) + +- 着色器语言 + + GLSL(OpenGL Shading Language)是OpenGL着色语言。 + + 在图形卡的GPU上执行,代替了固定的渲染管线的一部分,是渲染管线中不同层次具有可编程性,例如:视图转换、投影转换等。GLSL的着色器代码分为两个部分: + + - 顶点着色器(Vertex Shader) + - 片段着色器(Fragment Shader) + +- 坐标系 + + OpenGL ES采用虚拟坐标系 + + ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_es_xy.png) + + OpenGL ES采用的是右手坐标,选取屏幕中心为原点,从原点到屏幕边缘默认长度为1,也就是默认情况下,从原点到x左边的1和到y左边的1的位置在屏幕上显示的并不相同。 + +- 形状面和缠绕 + + 在OpenGL的世界里,我们只能画点、线、三角形,图元装配中说到所有复杂的图形都是由三角形组成。 + + ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_ex_xy_1.jpg) + + 三角形的点按顺序定义,使得它们以逆时针方向绘制。绘制这些坐标的顺序定义了形状的缠绕方向。默认情况下,在OpenGL中,逆时针绘制的面是正面。 + +- 程式(Program) + + 一个OpenGL ES对象,包含了你所希望用来绘制图形所要用到的着色器,最后顶点着色器和片元着色器都要放入到程式中,然后才可以使用,简单的说就是将两个着色器变为一个对象。 + + + +如果想要绘制图像,需要至少一个顶点着色器来定义一个图形顶点,以及一个片元着色器为该图形上色。这些着色器必须被编译然后再添加到一个OpenGL ES Program中,并利用这个Program来绘制形状。 + + + +### OpenGL Context + +OpenGL是一个仅仅关注图像渲染的图像接口库,在渲染过程中它需要将顶点信息、纹理信息、编译好的着色器等渲染状态信息存储起来,而存储这些信调用任何OpenGL函数前,必须先创建OpenGL Context,它存储了OpenGL的状态变量以及其他渲染有关的信息。OpenGL是个状态机,有很多状态变量,试个标准的过程式操作过程,改变状态会影响后续所有的操作,这和面向对象的解耦原则不符,毕竟渲染本身就是个复杂的过程。OpenGL采用Client-Server模型来解释OpenGL程序,即Server存储OpenGL Context,Client提出渲染请求,Server给予响应。之后的渲染工作就要依赖这些渲染状态信息来完成,当一个上下文被销毁时,它所对应的OpenGL渲染工作也将结束。 + + + +### EGL + +在OpenGL的设计中,OpenGL是不负责管理窗口的,窗口的管理交由各个设备自己完成。具体的来说就是iOS平台上使用EAGL提供本地平台对OpenGL的实现,在Android平台上使用EGL提供本地平台对OpenGL的实现。OpenGL其实是通过GPU进行渲染。但是我们的程序是运行在CPU上,要与GPU关联,这就需要通过EGL,它相当于Android上层应用于GPU通讯的中间层。 + +EGL为双缓冲工作模式,既有一个Back Frame Buffer和一个Front Frame Buffer,正常绘制的目标都是Back Frame Buffer,绘制完成后再调用eglSwapBuffer API,将绘制完毕的FrameBuffer交换到Front Frame Buffer并显示出来。 + +要在Android平台实现OpenGL渲染,需要完成一系列的EGL操作,主要为下面几步: + +1. 获取显示设备(EGL Display) + + 获取将要用于显示的设备,有些系统具有多个显示器,会存在多个display,在Android上通过调用EGL10的eglGetDisplay(Object native_display)方法获得EGLDisplay对象,通常传入的参数为EGL10.EGL_DEFAULT_DISPLAY。 + +2. 初始化EGL + + 调用EGL10的egInitialize(EGLDisplay display, int[] major_minor)方法完成初始化操作。display参数即为上一步获取的对象,major_minor传入的是一个int数据,通常传入的是一个大小为2的数据。 + +3. 选择Config配置 + + 调用EGL10的eglChooseConfig(EGLDisplay display, int[] attire_list, EGLConfig[] configs, int config_size, int[] num_config)方法,参数3用于存放输出的configs,参数4指定最多输出多少个config,参数5由EGL系统写入,表明满足attributes的config一共有多少个。 + +4. 创建EGL Context + + eglCreateContext(EGLDisplay display, EGLConfig config, EGLContext share_context, int[] attrib_list);参数1即为上面获取的Display,参数2为上一步chooseConfig传入的configs,share_context,是否有context共享,共享的contxt之间亦共享所有数据,通常设置为EGL_NO_CONTEXT代表不共享。attrib_list为int数组 {EGL_CONTEXT_CLIENT_VERSION, 2,EGL10.EGL_NONE };中间的2代表的是OpenGL ES的版本。 + +5. 创建EGLSurface + + eglCreateWindowSurface(EGLDisplay display, EGLConfig config, Object native_window, int[] attrib_list);参数1、2均为上述步骤得到的结果,参数3为上层创建的用于绘制内容的surface对象,参数4常设置为null。 + +6. 设置OpenGL的渲染环境 + + eglMakeCurrent(EGLDisplay display, EGLSurface draw, EGLSurface read, EGLContext context);该方法的参数意义很明确,该方法在异步线程中被调用,该线程也会被成为GL线程,一旦设定后,所有OpenGL渲染相关的操作都必须放在该线程中执行。 + + 通过上述操作,就完成了EGL的初始化设置,便可以进行OpenGL的渲染操作。 + + + +#### OpenGL纹理 + +纹理(Texture)是一个2D图片(也有1D和3D纹理),他可以用来添加物体的细节,你可以想象纹理是一张绘有砖块的纸,无缝折叠贴合到你的3D房子上,这样你的房子看起来就有砖墙的外表了。 + + + +Android 通过其框架 API 和原生开发套件 (NDK) 来支持 OpenGL。 + +Android 框架中有如下两个基本类,用于通过 OpenGL ES API 来创建和操控图形:`GLSurfaceView` 和 `GLSurfaceView.Renderer`。 + + + + + +参考 + +--- + +- [https://blog.csdn.net/gongxiaoou/article/details/89199632](https://blog.csdn.net/gongxiaoou/article/details/89199632) +- [https://blog.csdn.net/junzia/category_6462864.html](https://blog.csdn.net/junzia/category_6462864.html) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/VideoDevelopment/OpenGL/10.OpenGL ES\346\273\244\351\225\234.md" "b/VideoDevelopment/OpenGL/10.OpenGL ES\346\273\244\351\225\234.md" new file mode 100644 index 00000000..026edc95 --- /dev/null +++ "b/VideoDevelopment/OpenGL/10.OpenGL ES\346\273\244\351\225\234.md" @@ -0,0 +1,60 @@ +## 10.OpenGL ES滤镜 + +滤镜其实就是利用纹理。 + +滤镜的编写主要是靠基类。 + +- 先抽取BaseFilter类,封装好每个滤镜的着色器及onDraw等方法 +- 不同的滤镜集成该BaseFilter类 +- 在Renderder接口的实现类中去创建不同的Filter类,然后在onDrawFrame方法中去调用该filter.onDraw方法 + +滤镜的不同大部分只需要修改片元着色器就可以了。 + + + +### 反色滤镜 + +RGB三个通道的颜色取反,而alpha通道不变。 + +### 灰色滤镜 + +让RGB三个通道的颜色取均值 + +### 位移滤镜 + +纹理默认传入的读取范围是(0,0)到(1,1)内的颜色值。如果对读取的位置进行调整修改,那么就可以做出各种各样的效果,例如缩放动画就是让读取的范围改成(-1,-1)到(2,2)。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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" new file mode 100644 index 00000000..d3d7b314 --- /dev/null +++ "b/VideoDevelopment/OpenGL/2.GLSurfaceView\347\256\200\344\273\213.md" @@ -0,0 +1,216 @@ +## 2.GLSurfaceView简介 + +Android 通过其框架 API 和原生开发套件 (NDK) 来支持 OpenGL。 + +Android 框架中有如下两个基本类,用于通过 OpenGL ES API 来创建和操控图形:`GLSurfaceView` 和 `GLSurfaceView.Renderer`。也就是说我们想在Android中使用OpenGL ES的最简单的方法就是将我们要显示的图像渲染到GLSurfaceView上,但它只是一个展示类,至于怎么渲染到上面我们就要自己去实现GLSurfaceView.Renderer来生成我们自己的渲染器从而完成渲染操作。 + +######## GLSurfaceView(使用OpenGL绘制的图形的视图容器) + +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。 + +#### GLSurfaceView常用方法 + +- setEGLContextClientVersion:设置OpenGL ES的版本,3.0则设置3 +- onPause:暂停渲染,最好在Activity、Fragment的onPause方法内调用,减少不必要的性能开销,避免不必要的崩溃 +- onResume:恢复渲染 +- setRender:设置渲染器 +- setRenderMode:设置渲染模式 +- requestRender:请求渲染,请求异步线程进行渲染,调用后不会立即进行渲染。渲染会回调到Renderer接口的onDrawFrame()方法 +- queueEvent:插入一个Runnable任务到后台渲染线程上执行。相应的,渲染线程中可以通过Activity的runOnUIThread的方法来传递事件给主线程去执行 + +#### GLSurfaceView.Renderer(可控制该视图中绘制的图形) + +此接口定义了在 `GLSurfaceView` 中绘制图形所需的方法。您必须将此接口的一个实现作为单独的类提供,并使用 `GLSurfaceView.setRenderer()` 将其附加到您的 `GLSurfaceView` 实例。 + +`GLSurfaceView.Renderer` 接口要求您实现以下方法: + +- `onSurfaceCreated()`:系统会在创建 `GLSurfaceView` 时调用一次此方法。使用此方法可执行仅需发生一次的操作,例如设置 OpenGL 环境参数或初始化 OpenGL 图形对象。 +- `onDrawFrame()`:完成绘制工作,每一帧图像的渲染都要在这里完成,系统会在每次重新绘制 `GLSurfaceView` 时调用此方法。请将此方法作为绘制(和重新绘制)图形对象的主要执行点。 +- `onSurfaceChanged()`:系统会在 `GLSurfaceView` 几何图形发生变化(包括 `GLSurfaceView` 大小发生变化或设备屏幕方向发生变化)时调用此方法。例如,系统会在设备屏幕方向由纵向变为横向时调用此方法。使用此方法可响应 `GLSurfaceView` 容器中的更改。 + + + +使用 `GLSurfaceView` 和 `GLSurfaceView.Renderer` 为 OpenGL ES 建立容器视图后,您便可以开始使用以下类调用 OpenGL API: + +OpenGL ES 3.0/3.1 API 软件包 + +- ``` + android.opengl: 此软件包提供了 OpenGL ES 3.0/3.1 类的接口。版本 3.0 从 Android 4.3(API 级别 18)开始可用。版本 3.1 从 Android 5.0(API 级别 21)开始可用。 + ``` + + - `GLES30` + - `GLES31` + - `GLES31Ext` ([Android Extension Pack](https://developer.android.google.cn/guide/topics/graphics/opengl#aep)) + + + +[SurfaceView与TextureView](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/SurfaceView%E4%B8%8ETextureView.md) + +```java +package android.opengl; +/** + * An implementation of SurfaceView that uses the dedicated surface for + * displaying OpenGL rendering. + */ + public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback2 { + } + +``` + +GPU加速:GLSurfaceView的效率是SurfaceView的30倍以上,SurfaceView使用画布进行绘制,GLSurfaceView利用GPU加速提高了绘制效率。 +View的绘制onDraw(Canvas canvas)使用Skia渲染引擎渲染,而GLSurfaceView的渲染器Renderer的onDrawFrame(GL10 gl)使用opengl绘制引擎进行渲染。 + + + +GLSurfaceView的特性: + +- 管理一个surface,这个surface就是一块特殊的内存,能直接排版到android的视图view上。 +- 管理一个EGL Display,它能让OpenGL把内容渲染到surface上 +- 用户自定义渲染器render +- 让渲染器在独立的变成里运作(与UI线程分离) +- 支持按需渲染(on-demand)和连续渲染(continuous) +- 可以封装、跟踪并且排查渲染器的问题 + + + +#### OpenGL实现一个绿色的activity + +这里是全屏渲染成绿色,所以不会涉及到顶点着色器和片段着色器。 + +首选现在AndroidManifest.xml文件中声明使用OpenGL ES的版本: + +``` + +``` + + + +``` +public class MainActivity extends AppCompatActivity { + private GLSurfaceView mGlSurfaceView; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mGlSurfaceView = new MyGLSurfaceView(this); + setContentView(mGlSurfaceView); + } +} + +class MyGLSurfaceView extends GLSurfaceView { + private final MyGLRenderer renderer; + public MyGLSurfaceView(Context context) { + super(context); + // 设置opengl es的版本,现在应该没有app还要支持4.3一下系统了,直接用3.0就可以 + setEGLContextClientVersion(3); + renderer = new MyGLRenderer(); + setRenderer(renderer); + } +} + +class MyGLRenderer implements GLSurfaceView.Renderer { + public void onSurfaceCreated(GL10 unused, EGLConfig config) { + // Set the background frame color RGBA + GLES30.glClearColor(0.0f, 1.0f, 0.0f, 0.0f); + } + public void onDrawFrame(GL10 unused) { + // Redraw background color + // glClearColor设置好清除颜色,glClear利用glClearColor设置好的清除颜色来设置颜色缓冲区的颜色 + GLES30.glClear(GLES31.GL_COLOR_BUFFER_BIT); + } + + public void onSurfaceChanged(GL10 unused, int width, int height) { + GLES30.glViewport(0, 0, width, height); + } +} +``` + +效果图就不贴了,就是一个纯绿色的Activity。 + +Render接口重写的三个方法中调用了GLES31的API方法: + +- glClearColor():设置清空屏幕用的颜色,参数为RGBA。 +- glClear():清空屏幕,清空屏幕后调用glClearColor()中设置的颜色填充屏幕。 +- glViewport():设置视图的尺寸,告诉OpenGL可以用来渲染surface的大小。 + + + +#### RenderMode + +GLSurfaceView默认采用的是RENDERMODE_CONTINUOUSLY连续渲染的方式,刷新的帧率是60FPS,16ms就重绘一次,可以通过mGLView.setRenderMode()更改其渲染方式为RENDERMODE_WHEN_DIRTY,表示被动渲染,在surfaceCreate的时候会绘制一次,之后只有在调用requestRender或者onResume等方法主动请求重绘时才会进行渲染,如果你的界面不需要频繁的刷新最好使用RENDERMODE_WHEN_DIRTY,这样可以降低CPU和GPU的活动。 + +#### 状态处理 + +使用GLSurfaceView需要注意程序的生命周期,Activity及Fragment会有暂停和恢复等状态,GLSurfaceView也需根据这些状态来做相应的处理,GLSurfaceView具有onResume和onPause两个同Activity及Fragment中的生命周期同名的方法,在Activity或者Fragment中的onResume和onPause方法中,需要主动调用GLSurfaceView的实例的这两个方法,这样能使OpenGLES的内部线程做出正确的判断,从而保证应用程序的稳定性。 + + +#### GLSurfaceView的事件处理 + +为了处理事件,需要继承GLSurfaceView类并重载它的事件方法,但是由于GLSurfaceView是多线程的,渲染器在独立的渲染线程中,需要使用Java的跨线程机制与渲染器进行通讯,GLSurfaceView提供了queueEvent(Runnable runnable)方法就是一种相对简单的操作,queueEvent()方法被安全的用于在UI线程和渲染线程之间进行交流通信。这块在注释里面写了: + +``` + * To handle an event you will typically subclass GLSurfaceView and override the + * appropriate method, just as you would with any other View. However, when handling + * the event, you may need to communicate with the Renderer object + * that's running in the rendering thread. You can do this using any + * standard Java cross-thread communication mechanism. In addition, + * one relatively easy way to communicate with your renderer is + * to call + * {@link #queueEvent(Runnable)}. For example: + *
+ * class MyGLSurfaceView extends GLSurfaceView {
+ *
+ *     private MyRenderer mMyRenderer;
+ *
+ *     public void start() {
+ *         mMyRenderer = ...;
+ *         setRenderer(mMyRenderer);
+ *     }
+ *
+ *     public boolean onKeyDown(int keyCode, KeyEvent event) {
+ *         if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
+ *             queueEvent(new Runnable() {
+ *                 // This method will be called on the rendering
+ *                 // thread:
+ *                 public void run() {
+ *                     mMyRenderer.handleDpadCenter();
+ *                 }});
+ *             return true;
+ *         }
+ *         return super.onKeyDown(keyCode, event);
+ *     }
+ *}
+```
+
+
+
+### SurfaceTexture
+
+说到GLSurfaceView就一定要提一下SurfaceTexture。
+
+和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对象可以在任何线程上创建。 updateTexImage()只能在包含纹理对象的OpenGL ES上下文的线程上调用。 在任意线程上调用frame-available回调函数,不与updateTexImage()在同一线程上出现。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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"
new file mode 100644
index 00000000..c4c11e8b
--- /dev/null
+++ "b/VideoDevelopment/OpenGL/3.GLSurfaceView\346\272\220\347\240\201\350\247\243\346\236\220.md"
@@ -0,0 +1,690 @@
+GLSurfaceView源码解析
+
+我感觉还是先看一下源码,了解一下内部的流程,再接着学习其他的OpenGL部分会更合适。
+
+从上一篇文章中GLSurfaceView的使用中可以看到入口是setRenderer()方法,这里就卡一下setRenderder(Renderer renderer)方法的实现:  
+
+- Renderer接口
+
+```java
+public interface Renderer {
+		// surface创建的回调
+    void onSurfaceCreated(GL10 gl, EGLConfig config);
+		// surface大小改变的回调
+    void onSurfaceChanged(GL10 gl, int width, int height);
+		// 开始绘制每一帧的回调
+    void onDrawFrame(GL10 gl);
+}
+```
+
+- setRenderer()方法
+
+```java
+   public void setRenderer(Renderer renderer) {
+     		// 保证setRenderer方法只能被调用一次
+        checkRenderThreadState();
+        if (mEGLConfigChooser == null) {
+            // 设置EGLConfgi,如果不设置就用默认的一个RGB_888的Surface选择器
+            mEGLConfigChooser = new SimpleEGLConfigChooser(true);
+        }
+        if (mEGLContextFactory == null) {
+          	// 设置EGLContext工厂,如果不设置就用默认的EGLContext的工厂类,用他来创建EGLContext
+            mEGLContextFactory = new DefaultContextFactory();
+        }
+        if (mEGLWindowSurfaceFactory == null) {
+            // 设置EGLSurface工厂,如果不设置就用默认的EGLSurface的工厂类,我们可以通过这个方法来设置让图像渲染到其他地方的surface上,例如textureview上
+            mEGLWindowSurfaceFactory = new DefaultWindowSurfaceFactory();
+        }
+        mRenderer = renderer;
+        // 创建并开启GLThread,GL线程
+        mGLThread = new GLThread(mThisWeakRef);
+        mGLThread.start();
+    }
+```
+
+- 接下来看一下GLThread的线程的实现,GLThread是GLSurfaceView自带的一个渲染线程,同步的,不会阻塞线程,主要用来执行OpenGL的绘制工作:
+
+  ```java
+  static class GLThread extends Thread {
+          GLThread(WeakReference glSurfaceViewWeakRef) {
+              super();
+              mWidth = 0;
+              mHeight = 0;
+            	// 主动渲染模式下是true,如果是被动渲染模式就为false,然后通过requesetRender()方法来修改它的值
+              mRequestRender = true;
+              // 默认的渲染模式
+              mRenderMode = RENDERMODE_CONTINUOUSLY;
+              mWantRenderNotification = false;
+              mGLSurfaceViewWeakRef = glSurfaceViewWeakRef;
+          }
+  
+          @Override
+          public void run() {
+              setName("GLThread " + getId());
+              if (LOG_THREADS) {
+                  Log.i("GLThread", "starting tid=" + getId());
+              }
+  
+              try {
+                  // 具体的run方法,内部实现在下面,所有的核心都在里面,把这个方法单独放到下面说
+                  guardedRun();
+              } catch (InterruptedException e) {
+                  // fall thru and exit normally
+              } finally {
+                  sGLThreadManager.threadExiting(this);
+              }
+          }
+    				// GLSurfaceView的onPause方法会调用到这里
+            public void onPause() {
+              synchronized (sGLThreadManager) {
+                  if (LOG_PAUSE_RESUME) {
+                      Log.i("GLThread", "onPause tid=" + getId());
+                  }
+  
+                  mRequestPaused = true;
+                  // 这里使用了GLThreadManager这个类,这个类是提供线程同步控制功能的,最后会说这个类,通知EGL线程解除堵塞
+                  sGLThreadManager.notifyAll();
+                	// 保证onPause方法执行后GLThread也是pause的状态
+                  while ((! mExited) && (! mPaused)) {
+                      if (LOG_PAUSE_RESUME) {
+                          Log.i("Main thread", "onPause waiting for mPaused.");
+                      }
+                      try {
+                          sGLThreadManager.wait();
+                      } catch (InterruptedException ex) {
+                          Thread.currentThread().interrupt();
+                      }
+                  }
+              }
+          }
+  				 // GLSurfaceView的onResume方法会调用到这里
+          public void onResume() {
+              synchronized (sGLThreadManager) {
+                  if (LOG_PAUSE_RESUME) {
+                      Log.i("GLThread", "onResume tid=" + getId());
+                  }
+                  // 修改状态变量
+                  mRequestPaused = false;
+                  // 重绘一次
+                  mRequestRender = true;
+                  mRenderComplete = false;
+                  sGLThreadManager.notifyAll();
+                	// 保证onResume执行后,GLThread也是resume的状态
+                  while ((! mExited) && mPaused && (!mRenderComplete)) {
+                      if (LOG_PAUSE_RESUME) {
+                          Log.i("Main thread", "onResume waiting for !mPaused.");
+                      }
+                      try {
+                          sGLThreadManager.wait();
+                      } catch (InterruptedException ex) {
+                          Thread.currentThread().interrupt();
+                      }
+                  }
+              }
+          }
+      }
+  ```
+
+
+
+- guardeRun()方法:   
+
+  核心就是用一个while true循环,内部加了各种场景的判断,最后会调用renderer.onDrawFrame()方法进行绘制。
+
+  
+
+  ```java
+  private void guardedRun() throws InterruptedException {
+  						//GL帮助类,内部建立GL环境
+              mEglHelper = new EglHelper(mGLSurfaceViewWeakRef);
+              mHaveEglContext = false;
+              mHaveEglSurface = false;
+              mWantRenderNotification = false;
+  
+              try {
+                  GL10 gl = null;
+                  boolean createEglContext = false;
+                  boolean createEglSurface = false;
+                  boolean createGlInterface = false;
+                  boolean lostEglContext = false;
+                  boolean sizeChanged = false;
+                  boolean wantRenderNotification = false;
+                  boolean doRenderNotification = false;
+                  boolean askedToReleaseEglContext = false;
+                  int w = 0;
+                  int h = 0;
+                  Runnable event = null;
+                  Runnable finishDrawingRunnable = null;
+  
+                  while (true) {
+                      synchronized (sGLThreadManager) {
+                          while (true) {
+                          		// 外部请求退出
+                              if (mShouldExit) {
+                                  return;
+                              }
+  														// 如果还有GL线程中要处理的事件没处理完,就先处理事件
+                              if (! mEventQueue.isEmpty()) {
+                                  event = mEventQueue.remove(0);
+                                  break;
+                              }
+  														// 更新onResume和onPause时的状态变化
+                              // Update the pause state.
+                              boolean pausing = false;
+                              if (mPaused != mRequestPaused) {
+                                  pausing = mRequestPaused;
+                                  mPaused = mRequestPaused;
+                                  // onPause和onResume的时候都会用wait方法等待GL线程的响应,这时候主线程堵塞。需要调用notifyAll
+                                  sGLThreadManager.notifyAll();
+                                  if (LOG_PAUSE_RESUME) {
+                                      Log.i("GLThread", "mPaused is now " + mPaused + " tid=" + getId());
+                                  }
+                              }
+  														// 需要释放EGLContext
+                              // Do we need to give up the EGL context?
+                              if (mShouldReleaseEglContext) {
+                                  if (LOG_SURFACE) {
+                                      Log.i("GLThread", "releasing EGL context because asked to tid=" + getId());
+                                  }
+                                  stopEglSurfaceLocked();
+                                  stopEglContextLocked();
+                                  mShouldReleaseEglContext = false;
+                                  askedToReleaseEglContext = true;
+                              }
+  		                        // 如果EGLContext丢失,需要销毁EGLSurface和EGLContext
+                              // Have we lost the EGL context?
+                              if (lostEglContext) {
+                                  stopEglSurfaceLocked();
+                                  stopEglContextLocked();
+                                  lostEglContext = false;
+                              }
+  														// 如果onPause了并且当前GLSurface已经存在了,就销毁EGLSurface
+                              // When pausing, release the EGL surface:
+                              if (pausing && mHaveEglSurface) {
+                                  if (LOG_SURFACE) {
+                                      Log.i("GLThread", "releasing EGL surface because paused tid=" + getId());
+                                  }
+                                  stopEglSurfaceLocked();
+                              }
+  														// 接受了onPuase信号,并且当前EGLContext存在时,需要根据用户的设置来决定是否销毁EGLContext
+                              // When pausing, optionally release the EGL Context:
+                              if (pausing && mHaveEglContext) {
+                                  GLSurfaceView view = mGLSurfaceViewWeakRef.get();
+                                  boolean preserveEglContextOnPause = view == null ?
+                                          false : view.mPreserveEGLContextOnPause;
+                                  if (!preserveEglContextOnPause) {
+                                      stopEglContextLocked();
+                                      if (LOG_SURFACE) {
+                                          Log.i("GLThread", "releasing EGL context because paused tid=" + getId());
+                                      }
+                                  }
+                              }
+  														
+                              // Have we lost the SurfaceView surface?
+                              if ((! mHasSurface) && (! mWaitingForSurface)) {
+                                  if (LOG_SURFACE) {
+                                      Log.i("GLThread", "noticed surfaceView surface lost tid=" + getId());
+                                  }
+                                  if (mHaveEglSurface) {
+                                      stopEglSurfaceLocked();
+                                  }
+                                  mWaitingForSurface = true;
+                                  mSurfaceIsBad = false;
+                                  sGLThreadManager.notifyAll();
+                              }
+  
+                              // Have we acquired the surface view surface?
+                              if (mHasSurface && mWaitingForSurface) {
+                                  if (LOG_SURFACE) {
+                                      Log.i("GLThread", "noticed surfaceView surface acquired tid=" + getId());
+                                  }
+                                  mWaitingForSurface = false;
+                                  sGLThreadManager.notifyAll();
+                              }
+  
+                              if (doRenderNotification) {
+                                  if (LOG_SURFACE) {
+                                      Log.i("GLThread", "sending render notification tid=" + getId());
+                                  }
+                                  mWantRenderNotification = false;
+                                  doRenderNotification = false;
+                                  mRenderComplete = true;
+                                  sGLThreadManager.notifyAll();
+                              }
+  
+                              if (mFinishDrawingRunnable != null) {
+                                  finishDrawingRunnable = mFinishDrawingRunnable;
+                                  mFinishDrawingRunnable = null;
+                              }
+  														// readyToDraw()方法内部会判断是否已经pause或者mRequestRender是否是true以及是否是主动渲染模式。如果是被动渲染的模式mRequestRender就会是false,只有调用requestRender()方法后才会是true,如果是主动渲染模式readyToDraw()就不用根据mRequestRender来判断。
+                              // Ready to draw?
+                              if (readyToDraw()) {
+  																// 没有EGLContext就去调用EGLHelper来创建,第一次会走到这里先创建EGLContext
+                                  // If we don't have an EGL context, try to acquire one.
+                                  if (! mHaveEglContext) {
+                                      if (askedToReleaseEglContext) {
+                                          askedToReleaseEglContext = false;
+                                      } else {
+                                          try {
+                                              // 后面会看这个EglHelper.start()方法,初始化EGL环境,内部会去创建各种GL EGL EGLContext EGLDisplay等环境
+                                              mEglHelper.start();
+                                          } catch (RuntimeException t) {
+                                              sGLThreadManager.releaseEglContextLocked(this);
+                                              throw t;
+                                          }
+                                        	// 创建完后把该变量变成true
+                                          mHaveEglContext = true;
+                                          createEglContext = true;
+  
+                                          sGLThreadManager.notifyAll();
+                                      }
+                                  }
+  
+                                  if (mHaveEglContext && !mHaveEglSurface) {
+                                      mHaveEglSurface = true;
+                                      createEglSurface = true;
+                                      createGlInterface = true;
+                                      sizeChanged = true;
+                                  }
+  
+                                  if (mHaveEglSurface) {
+                                      if (mSizeChanged) {
+                                          sizeChanged = true;
+                                          w = mWidth;
+                                          h = mHeight;
+                                          mWantRenderNotification = true;
+                                          if (LOG_SURFACE) {
+                                              Log.i("GLThread",
+                                                      "noticing that we want render notification tid="
+                                                      + getId());
+                                          }
+  
+                                          // Destroy and recreate the EGL surface.
+                                          createEglSurface = true;
+  
+                                          mSizeChanged = false;
+                                      }
+                                      mRequestRender = false;
+                                      sGLThreadManager.notifyAll();
+                                      if (mWantRenderNotification) {
+                                          wantRenderNotification = true;
+                                      }
+                                      break;
+                                  }
+                              } else {
+                                  if (finishDrawingRunnable != null) {
+                                      Log.w(TAG, "Warning, !readyToDraw() but waiting for " +
+                                              "draw finished! Early reporting draw finished.");
+                                      finishDrawingRunnable.run();
+                                      finishDrawingRunnable = null;
+                                  }
+                              }
+                              // By design, this is the only place in a GLThread thread where we wait().
+                              sGLThreadManager.wait();
+                          }
+                      } // end of synchronized(sGLThreadManager)
+  
+                      if (event != null) {
+                         // 执行event的run方法,这个event是GLSurfaceView中提供的queueEvent(runnable run)实现的,该方法会把runnable添加到EventQueue中,然后再在这里去执行该runnable
+                          event.run();
+                          event = null;
+                          continue;
+                      }
+  
+                      if (createEglSurface) {
+  												// EglHelper创建EglSurface                      
+                          if (mEglHelper.createSurface()) {
+                              synchronized(sGLThreadManager) {
+                                  mFinishedCreatingEglSurface = true;
+                                  sGLThreadManager.notifyAll();
+                              }
+                          } else {
+                              synchronized(sGLThreadManager) {
+                                  mFinishedCreatingEglSurface = true;
+                                  mSurfaceIsBad = true;
+                                  sGLThreadManager.notifyAll();
+                              }
+                              continue;
+                          }
+                          createEglSurface = false;
+                      }
+  
+                      if (createGlInterface) {
+                          gl = (GL10) mEglHelper.createGL();
+  
+                          createGlInterface = false;
+                      }
+  
+                      if (createEglContext) {
+                          if (LOG_RENDERER) {
+                              Log.w("GLThread", "onSurfaceCreated");
+                          }
+                          GLSurfaceView view = mGLSurfaceViewWeakRef.get();
+                          if (view != null) {
+                              try {
+                                  Trace.traceBegin(Trace.TRACE_TAG_VIEW, "onSurfaceCreated");
+                                	// 回调onSurfaceCreated
+                                  view.mRenderer.onSurfaceCreated(gl, mEglHelper.mEglConfig);
+                              } finally {
+                                  Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+                              }
+                          }
+                          createEglContext = false;
+                      }
+  
+                      if (sizeChanged) {
+                          if (LOG_RENDERER) {
+                              Log.w("GLThread", "onSurfaceChanged(" + w + ", " + h + ")");
+                          }
+                          GLSurfaceView view = mGLSurfaceViewWeakRef.get();
+                          if (view != null) {
+                              try {
+                                  Trace.traceBegin(Trace.TRACE_TAG_VIEW, "onSurfaceChanged");
+                                  // 回调onSurfaceChangeed
+                                  view.mRenderer.onSurfaceChanged(gl, w, h);
+                              } finally {
+                                  Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+                              }
+                          }
+                          sizeChanged = false;
+                      }
+  
+                      if (LOG_RENDERER_DRAW_FRAME) {
+                          Log.w("GLThread", "onDrawFrame tid=" + getId());
+                      }
+                      {
+                          GLSurfaceView view = mGLSurfaceViewWeakRef.get();
+                          if (view != null) {
+                              // 调用renderder.onDrawFrame方法开始绘制
+                              try {
+                                  Trace.traceBegin(Trace.TRACE_TAG_VIEW, "onDrawFrame");
+                                  view.mRenderer.onDrawFrame(gl);
+                                  if (finishDrawingRunnable != null) {
+                                      finishDrawingRunnable.run();
+                                      finishDrawingRunnable = null;
+                                  }
+                              } finally {
+                                  Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+                              }
+                          }
+                      }
+                      int swapError = mEglHelper.swap();
+                      switch (swapError) {
+                          case EGL10.EGL_SUCCESS:
+                              break;
+                          case EGL11.EGL_CONTEXT_LOST:
+                              if (LOG_SURFACE) {
+                                  Log.i("GLThread", "egl context lost tid=" + getId());
+                              }
+                              lostEglContext = true;
+                              break;
+                          default:
+                              // Other errors typically mean that the current surface is bad,
+                              // probably because the SurfaceView surface has been destroyed,
+                              // but we haven't been notified yet.
+                              // Log the error to help developers understand why rendering stopped.
+                              EglHelper.logEglErrorAsWarning("GLThread", "eglSwapBuffers", swapError);
+  
+                              synchronized(sGLThreadManager) {
+                                  mSurfaceIsBad = true;
+                                  sGLThreadManager.notifyAll();
+                              }
+                              break;
+                      }
+  
+                      if (wantRenderNotification) {
+                          doRenderNotification = true;
+                          wantRenderNotification = false;
+                      }
+                  }
+  
+              } finally {
+                  /*
+                   * clean-up everything...
+                   */
+                  synchronized (sGLThreadManager) {
+                      stopEglSurfaceLocked();
+                      stopEglContextLocked();
+                  }
+              }
+          }
+  ```
+
+接着看一下上面提到的几个类: 
+
+- EGLHelper
+
+```java
+/**
+     * An EGL helper class.
+     */
+
+    private static class EglHelper {
+        public EglHelper(WeakReference glSurfaceViewWeakRef) {
+            mGLSurfaceViewWeakRef = glSurfaceViewWeakRef;
+        }
+
+        /**
+         * 初始化EGL环境,创建EGL EGLContext EGLDisplay
+         * Initialize EGL for a given configuration spec.
+         * @param configSpec
+         */
+        public void start() {
+            if (LOG_EGL) {
+                Log.w("EglHelper", "start() tid=" + Thread.currentThread().getId());
+            }
+            /*
+             * Get an EGL instance
+             */
+            mEgl = (EGL10) EGLContext.getEGL();
+
+            /*
+             * Get to the default display.
+             */
+            mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
+
+            if (mEglDisplay == EGL10.EGL_NO_DISPLAY) {
+                throw new RuntimeException("eglGetDisplay failed");
+            }
+
+            /*
+             * We can now initialize EGL for that display
+             */
+            int[] version = new int[2];
+            if(!mEgl.eglInitialize(mEglDisplay, version)) {
+                throw new RuntimeException("eglInitialize failed");
+            }
+            GLSurfaceView view = mGLSurfaceViewWeakRef.get();
+            if (view == null) {
+                mEglConfig = null;
+                mEglContext = null;
+            } else {
+                // 使用setRender方法中setEGLConfigChooser和setEGLContextFactory中设置的部分来创建
+                mEglConfig = view.mEGLConfigChooser.chooseConfig(mEgl, mEglDisplay);
+
+                /*
+                * Create an EGL context. We want to do this as rarely as we can, because an
+                * EGL context is a somewhat heavy object.
+                */
+                mEglContext = view.mEGLContextFactory.createContext(mEgl, mEglDisplay, mEglConfig);
+            }
+            if (mEglContext == null || mEglContext == EGL10.EGL_NO_CONTEXT) {
+                mEglContext = null;
+                throwEglException("createContext");
+            }
+            if (LOG_EGL) {
+                Log.w("EglHelper", "createContext " + mEglContext + " tid=" + Thread.currentThread().getId());
+            }
+
+            mEglSurface = null;
+        }
+
+        /**
+         * 创建EGLSurface
+         * Create an egl surface for the current SurfaceHolder surface. If a surface
+         * already exists, destroy it before creating the new surface.
+         *
+         * @return true if the surface was created successfully.
+         */
+        public boolean createSurface() {
+            if (LOG_EGL) {
+                Log.w("EglHelper", "createSurface()  tid=" + Thread.currentThread().getId());
+            }
+            /*
+             * Check preconditions.
+             */
+            if (mEgl == null) {
+                throw new RuntimeException("egl not initialized");
+            }
+            if (mEglDisplay == null) {
+                throw new RuntimeException("eglDisplay not initialized");
+            }
+            if (mEglConfig == null) {
+                throw new RuntimeException("mEglConfig not initialized");
+            }
+
+            /*
+             *  The window size has changed, so we need to create a new
+             *  surface.
+             */
+            destroySurfaceImp();
+
+            /*
+             * Create an EGL surface we can render into.
+             */
+            GLSurfaceView view = mGLSurfaceViewWeakRef.get();
+            if (view != null) {
+            		// 也是通过EGLWindowSurfaceFactory来创建EGLSurface,如果没有设置就会调用默认的DefaultWindowSurfaceFactory。后面说一下这里
+                mEglSurface = view.mEGLWindowSurfaceFactory.createWindowSurface(mEgl,
+                        mEglDisplay, mEglConfig, view.getHolder());
+            } else {
+                mEglSurface = null;
+            }
+
+            /*
+            将EGLContext上下文加载到当前线程环境
+             * Before we can issue GL commands, we need to make sure
+             * the context is current and bound to a surface.
+             */
+            if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) {
+                /*
+                 * Could not make the context current, probably because the underlying
+                 * SurfaceView surface has been destroyed.
+                 */
+                logEglErrorAsWarning("EGLHelper", "eglMakeCurrent", mEgl.eglGetError());
+                return false;
+            }
+
+            return true;
+        }
+
+        /**
+         获取OpenGL ES的编程接口
+         * Create a GL object for the current EGL context.
+         * @return
+         */
+        GL createGL() {
+            GL gl = mEglContext.getGL();
+            GLSurfaceView view = mGLSurfaceViewWeakRef.get();
+            if (view != null) {
+                if (view.mGLWrapper != null) {
+                    gl = view.mGLWrapper.wrap(gl);
+                }
+
+                if ((view.mDebugFlags & (DEBUG_CHECK_GL_ERROR | DEBUG_LOG_GL_CALLS)) != 0) {
+                    int configFlags = 0;
+                    Writer log = null;
+                    if ((view.mDebugFlags & DEBUG_CHECK_GL_ERROR) != 0) {
+                        configFlags |= GLDebugHelper.CONFIG_CHECK_GL_ERROR;
+                    }
+                    if ((view.mDebugFlags & DEBUG_LOG_GL_CALLS) != 0) {
+                        log = new LogWriter();
+                    }
+                    gl = GLDebugHelper.wrap(gl, configFlags, log);
+                }
+            }
+            return gl;
+        }
+        public void destroySurface() {
+            if (LOG_EGL) {
+                Log.w("EglHelper", "destroySurface()  tid=" + Thread.currentThread().getId());
+            }
+            destroySurfaceImp();
+        }
+
+        private void destroySurfaceImp() {
+            if (mEglSurface != null && mEglSurface != EGL10.EGL_NO_SURFACE) {
+                mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE,
+                        EGL10.EGL_NO_SURFACE,
+                        EGL10.EGL_NO_CONTEXT);
+                GLSurfaceView view = mGLSurfaceViewWeakRef.get();
+                if (view != null) {
+                		// EGLWindowSurfaceFactory除了创建EGLSurface还有销毁的功能
+                    view.mEGLWindowSurfaceFactory.destroySurface(mEgl, mEglDisplay, mEglSurface);
+                }
+                mEglSurface = null;
+            }
+        }
+        private WeakReference mGLSurfaceViewWeakRef;
+        EGL10 mEgl;
+        EGLDisplay mEglDisplay;
+        EGLSurface mEglSurface;
+        EGLConfig mEglConfig;
+        EGLContext mEglContext;
+
+    }
+```
+
+- EGLWindowSurfaceFactory
+
+```java
+    /**
+     * An interface for customizing the eglCreateWindowSurface and eglDestroySurface calls.
+     * 

+ * This interface must be implemented by clients wishing to call + * {@link GLSurfaceView#setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory)} + */ + public interface EGLWindowSurfaceFactory { + /** + * @return null if the surface cannot be constructed. + */ + EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display, EGLConfig config, + Object nativeWindow); + void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface); + } + + private static class DefaultWindowSurfaceFactory implements EGLWindowSurfaceFactory { + + public EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display, + EGLConfig config, Object nativeWindow) { + EGLSurface result = null; + try { + result = egl.eglCreateWindowSurface(display, config, nativeWindow, null); + } catch (IllegalArgumentException e) { + // This exception indicates that the surface flinger surface + // is not valid. This can happen if the surface flinger surface has + // been torn down, but the application has not yet been + // notified via SurfaceHolder.Callback.surfaceDestroyed. + // In theory the application should be notified first, + // but in practice sometimes it is not. See b/4588890 + Log.e(TAG, "eglCreateWindowSurface", e); + } + return result; + } + + public void destroySurface(EGL10 egl, EGLDisplay display, + EGLSurface surface) { + egl.eglDestroySurface(display, surface); + } + } + +``` + +看到这里我们知道可以自定义WindowSurfaceFactory然后使用其他SurfaceView或者TextureView的Surface来创建EGLSurface,让其把渲染后的内容输出到SurfaceView或者TextureView上显示。 + +- GLThreadManager类 + + 上面看到wait和notifyall的时候sGLThreadManager.notifyAll();都用到了GLThreadManager类。它就是线程同步的锁。 + + 这个类主要提供的是线程同步控制的功能,因为在GLSurfaceView里面有两个线程: GL线程和调用线程。所以对一些变量必须要进行同步。 + 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" new file mode 100644 index 00000000..6f5b93fe --- /dev/null +++ "b/VideoDevelopment/OpenGL/4.GLTextureView\345\256\236\347\216\260.md" @@ -0,0 +1,1974 @@ +### GLTextureView + +系统提供了GLSurfaceView,确没有提供GLTextureView,但是我们目前的项目灰常复杂庞大,我不想去封一层接口,然后动态去选择使用GLSurfaceView(实现一些纹理效果)或者TextureView(无OpenGL效果)来播放视频。我只想在目前的基础上去扩展,我想去实现一个GLTextureView。上面分析完GLSurfaceView的源码后我们也可以自己去实现GLTextureView的功能了。具体的实现就是和GLSurfaceView的源码一模一样。 + +我对里面的改动点主要有两个地方,这俩是为了让render传null的时候可以当做普通的TextureView来使用: + +- ```java + if (renderer == null) { + // 与普通TextureView一样 + return; + } + ``` + +- ```java + @Override + public void setSurfaceTextureListener(SurfaceTextureListener listener) { + if (mRenderer == null) { + // 与普通TextureView一样 + super.setSurfaceTextureListener(listener); + } + Log.e(TAG, "setSurfaceTextureListener preserved, setRenderer() instead?"); + } + ``` + +```java +/** + * An implementation of TextureView that uses the dedicated surface for + * displaying OpenGL rendering. + *

+ * A GLTextureView provides the following features: + *

+ *

    + *
  • Manages a surface, which is a special piece of memory that can be + * composited into the Android view system. + *
  • Manages an EGL display, which enables OpenGL to render into a surface. + *
  • Accepts a user-provided Renderer object that does the actual rendering. + *
  • Renders on a dedicated thread to decouple rendering performance from the + * UI thread. + *
  • Supports both on-demand and continuous rendering. + *
  • Optionally wraps, traces, and/or error-checks the renderer's OpenGL calls. + *
+ * + *
+ *

Developer Guides

+ *

For more information about how to use OpenGL, read the + * OpenGL developer guide.

+ *
+ * + *

Using GLTextureView

+ *

+ * Typically you use GLTextureView by subclassing it and overriding one or more of the + * View system input event methods. If your application does not need to override event + * methods then GLTextureView can be used as-is. For the most part + * GLTextureView behavior is customized by calling "set" methods rather than by subclassing. + * For example, unlike a regular View, drawing is delegated to a separate Renderer object which + * is registered with the GLTextureView + * using the {@link #setRenderer(Renderer)} call. + *

+ *

Initializing GLTextureView

+ * All you have to do to initialize a GLTextureView is call {@link #setRenderer(Renderer)}. + * However, if desired, you can modify the default behavior of GLTextureView by calling one or + * more of these methods before calling setRenderer: + *
    + *
  • {@link #setDebugFlags(int)} + *
  • {@link #setEGLConfigChooser(boolean)} + *
  • {@link #setEGLConfigChooser(EGLConfigChooser)} + *
  • {@link #setEGLConfigChooser(int, int, int, int, int, int)} + *
  • {@link #setGLWrapper(GLWrapper)} + *
+ *

+ *

Specifying the android.view.Surface

+ * By default GLTextureView will create a PixelFormat.RGB_888 format surface. If a translucent + * surface is required, call getHolder().setFormat(PixelFormat.TRANSLUCENT). + * The exact format of a TRANSLUCENT surface is device dependent, but it will be + * a 32-bit-per-pixel surface with 8 bits per component. + *

+ *

Choosing an EGL Configuration

+ * A given Android device may support multiple EGLConfig rendering configurations. + * The available configurations may differ in how may channels of data are present, as + * well as how many bits are allocated to each channel. Therefore, the first thing + * GLTextureView has to do when starting to render is choose what EGLConfig to use. + *

+ * By default GLTextureView chooses a EGLConfig that has an RGB_888 pixel format, + * with at least a 16-bit depth buffer and no stencil. + *

+ * If you would prefer a different EGLConfig + * you can override the default behavior by calling one of the + * setEGLConfigChooser methods. + *

+ *

Debug Behavior

+ * You can optionally modify the behavior of GLTextureView by calling + * one or more of the debugging methods {@link #setDebugFlags(int)}, + * and {@link #setGLWrapper}. These methods may be called before and/or after setRenderer, but + * typically they are called before setRenderer so that they take effect immediately. + *

+ *

Setting a Renderer

+ * Finally, you must call {@link #setRenderer} to register a {@link Renderer}. + * The renderer is + * responsible for doing the actual OpenGL rendering. + *

+ *

Rendering Mode

+ * Once the renderer is set, you can control whether the renderer draws + * continuously or on-demand by calling + * {@link #setRenderMode}. The default is continuous rendering. + *

+ *

Activity Life-cycle

+ * A GLTextureView must be notified when the activity is paused and resumed. GLTextureView clients + * are required to call {@link #onPause()} when the activity pauses and + * {@link #onResume()} when the activity resumes. These calls allow GLTextureView to + * pause and resume the rendering thread, and also allow GLTextureView to release and recreate + * the OpenGL display. + *

+ *

Handling events

+ *

+ * To handle an event you will typically subclass GLTextureView and override the + * appropriate method, just as you would with any other View. However, when handling + * the event, you may need to communicate with the Renderer object + * that's running in the rendering thread. You can do this using any + * standard Java cross-thread communication mechanism. In addition, + * one relatively easy way to communicate with your renderer is + * to call + * {@link #queueEvent(Runnable)}. For example: + *

+ * class MyGLTextureView extends GLTextureView {
+ *
+ *     private MyRenderer mMyRenderer;
+ *
+ *     public void start() {
+ *         mMyRenderer = ...;
+ *         setRenderer(mMyRenderer);
+ *     }
+ *
+ *     public boolean onKeyDown(int keyCode, KeyEvent event) {
+ *         if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
+ *             queueEvent(new Runnable() {
+ *                 // This method will be called on the rendering
+ *                 // thread:
+ *                 public void run() {
+ *                     mMyRenderer.handleDpadCenter();
+ *                 }});
+ *             return true;
+ *         }
+ *         return super.onKeyDown(keyCode, event);
+ *     }
+ * }
+ * 
+ * + */ +public class GLTextureView extends TextureView implements TextureView.SurfaceTextureListener { + private final static String TAG = "GLTextureView"; + + private final static boolean LOG_ATTACH_DETACH = false; + private final static boolean LOG_THREADS = false; + private final static boolean LOG_PAUSE_RESUME = false; + private final static boolean LOG_SURFACE = false; + private final static boolean LOG_RENDERER = false; + private final static boolean LOG_RENDERER_DRAW_FRAME = false; + private final static boolean LOG_EGL = false; + /** + * The renderer only renders + * when the surface is created, or when {@link #requestRender} is called. + * + * @see #getRenderMode() + * @see #setRenderMode(int) + * @see #requestRender() + */ + public final static int RENDERMODE_WHEN_DIRTY = 0; + /** + * The renderer is called + * continuously to re-render the scene. + * + * @see #getRenderMode() + * @see #setRenderMode(int) + */ + public final static int RENDERMODE_CONTINUOUSLY = 1; + + /** + * Check glError() after every GL call and throw an exception if glError indicates + * that an error has occurred. This can be used to help track down which OpenGL ES call + * is causing an error. + * + * @see #getDebugFlags + * @see #setDebugFlags + */ + public final static int DEBUG_CHECK_GL_ERROR = 1; + + /** + * Log GL calls to the system log at "verbose" level with tag "GLTextureView". + * + * @see #getDebugFlags + * @see #setDebugFlags + */ + public final static int DEBUG_LOG_GL_CALLS = 2; + + /** + * Standard View constructor. In order to render something, you + * must call {@link #setRenderer} to register a renderer. + */ + public GLTextureView(Context context) { + super(context); + init(); + } + + /** + * Standard View constructor. In order to render something, you + * must call {@link #setRenderer} to register a renderer. + */ + public GLTextureView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + @Override + protected void finalize() throws Throwable { + try { + if (mGLThread != null) { + // GLThread may still be running if this view was never + // attached to a window. + mGLThread.requestExitAndWait(); + } + } finally { + super.finalize(); + } + } + + private void init() { + super.setSurfaceTextureListener(this); + } + + /** + * Set the glWrapper. If the glWrapper is not null, its + * {@link GLWrapper#wrap(GL)} method is called + * whenever a surface is created. A GLWrapper can be used to wrap + * the GL object that's passed to the renderer. Wrapping a GL + * object enables examining and modifying the behavior of the + * GL calls made by the renderer. + *

+ * Wrapping is typically used for debugging purposes. + *

+ * The default value is null. + * @param glWrapper the new GLWrapper + */ + public void setGLWrapper(GLWrapper glWrapper) { + mGLWrapper = glWrapper; + } + + /** + * Set the debug flags to a new value. The value is + * constructed by OR-together zero or more + * of the DEBUG_CHECK_* constants. The debug flags take effect + * whenever a surface is created. The default value is zero. + * @param debugFlags the new debug flags + * @see #DEBUG_CHECK_GL_ERROR + * @see #DEBUG_LOG_GL_CALLS + */ + public void setDebugFlags(int debugFlags) { + mDebugFlags = debugFlags; + } + + /** + * Get the current value of the debug flags. + * @return the current value of the debug flags. + */ + public int getDebugFlags() { + return mDebugFlags; + } + + /** + * Control whether the EGL context is preserved when the GLTextureView is paused and + * resumed. + *

+ * If set to true, then the EGL context may be preserved when the GLTextureView is paused. + * Whether the EGL context is actually preserved or not depends upon whether the + * Android device that the program is running on can support an arbitrary number of EGL + * contexts or not. Devices that can only support a limited number of EGL contexts must + * release the EGL context in order to allow multiple applications to share the GPU. + *

+ * If set to false, the EGL context will be released when the GLTextureView is paused, + * and recreated when the GLTextureView is resumed. + *

+ * + * The default is false. + * + * @param preserveOnPause preserve the EGL context when paused + */ + public void setPreserveEGLContextOnPause(boolean preserveOnPause) { + mPreserveEGLContextOnPause = preserveOnPause; + } + + /** + * @return true if the EGL context will be preserved when paused + */ + public boolean getPreserveEGLContextOnPause() { + return mPreserveEGLContextOnPause; + } + + /** + * Set the renderer associated with this view. Also starts the thread that + * will call the renderer, which in turn causes the rendering to start. + *

This method should be called once and only once in the life-cycle of + * a GLTextureView. + *

The following GLTextureView methods can only be called before + * setRenderer is called: + *

    + *
  • {@link #setEGLConfigChooser(boolean)} + *
  • {@link #setEGLConfigChooser(EGLConfigChooser)} + *
  • {@link #setEGLConfigChooser(int, int, int, int, int, int)} + *
+ *

+ * The following GLTextureView methods can only be called after + * setRenderer is called: + *

    + *
  • {@link #getRenderMode()} + *
  • {@link #onPause()} + *
  • {@link #onResume()} + *
  • {@link #queueEvent(Runnable)} + *
  • {@link #requestRender()} + *
  • {@link #setRenderMode(int)} + *
+ * + * @param renderer the renderer to use to perform OpenGL drawing. + */ + public void setRenderer(Renderer renderer) { + if (renderer == null) { + // 与普通TextureView一样 + return; + } + checkRenderThreadState(); + if (mEGLConfigChooser == null) { + mEGLConfigChooser = new SimpleEGLConfigChooser(true); + } + if (mEGLContextFactory == null) { + mEGLContextFactory = new DefaultContextFactory(); + } + if (mEGLWindowSurfaceFactory == null) { + mEGLWindowSurfaceFactory = new DefaultWindowSurfaceFactory(); + } + mRenderer = renderer; + mGLThread = new GLThread(mThisWeakRef); + mGLThread.start(); + } + + /** + * Install a custom EGLContextFactory. + *

If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + *

+ * If this method is not called, then by default + * a context will be created with no shared context and + * with a null attribute list. + */ + public void setEGLContextFactory(EGLContextFactory factory) { + checkRenderThreadState(); + mEGLContextFactory = factory; + } + + /** + * Install a custom EGLWindowSurfaceFactory. + *

If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + *

+ * If this method is not called, then by default + * a window surface will be created with a null attribute list. + */ + public void setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory factory) { + checkRenderThreadState(); + mEGLWindowSurfaceFactory = factory; + } + + /** + * Install a custom EGLConfigChooser. + *

If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + *

+ * If no setEGLConfigChooser method is called, then by default the + * view will choose an EGLConfig that is compatible with the current + * android.view.Surface, with a depth buffer depth of + * at least 16 bits. + * @param configChooser + */ + public void setEGLConfigChooser(EGLConfigChooser configChooser) { + checkRenderThreadState(); + mEGLConfigChooser = configChooser; + } + + /** + * Install a config chooser which will choose a config + * as close to 16-bit RGB as possible, with or without an optional depth + * buffer as close to 16-bits as possible. + *

If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + *

+ * If no setEGLConfigChooser method is called, then by default the + * view will choose an RGB_888 surface with a depth buffer depth of + * at least 16 bits. + * + * @param needDepth + */ + public void setEGLConfigChooser(boolean needDepth) { + setEGLConfigChooser(new SimpleEGLConfigChooser(needDepth)); + } + + /** + * Install a config chooser which will choose a config + * with at least the specified depthSize and stencilSize, + * and exactly the specified redSize, greenSize, blueSize and alphaSize. + *

If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + *

+ * If no setEGLConfigChooser method is called, then by default the + * view will choose an RGB_888 surface with a depth buffer depth of + * at least 16 bits. + * + */ + public void setEGLConfigChooser(int redSize, int greenSize, int blueSize, + int alphaSize, int depthSize, int stencilSize) { + setEGLConfigChooser(new ComponentSizeChooser(redSize, greenSize, + blueSize, alphaSize, depthSize, stencilSize)); + } + + /** + * Inform the default EGLContextFactory and default EGLConfigChooser + * which EGLContext client version to pick. + *

Use this method to create an OpenGL ES 2.0-compatible context. + * Example: + *

+     *     public MyView(Context context) {
+     *         super(context);
+     *         setEGLContextClientVersion(2); // Pick an OpenGL ES 2.0 context.
+     *         setRenderer(new MyRenderer());
+     *     }
+     * 
+ *

Note: Activities which require OpenGL ES 2.0 should indicate this by + * setting @lt;uses-feature android:glEsVersion="0x00020000" /> in the activity's + * AndroidManifest.xml file. + *

If this method is called, it must be called before {@link #setRenderer(Renderer)} + * is called. + *

This method only affects the behavior of the default EGLContexFactory and the + * default EGLConfigChooser. If + * {@link #setEGLContextFactory(EGLContextFactory)} has been called, then the supplied + * EGLContextFactory is responsible for creating an OpenGL ES 2.0-compatible context. + * If + * {@link #setEGLConfigChooser(EGLConfigChooser)} has been called, then the supplied + * EGLConfigChooser is responsible for choosing an OpenGL ES 2.0-compatible config. + * @param version The EGLContext client version to choose. Use 2 for OpenGL ES 2.0 + */ + public void setEGLContextClientVersion(int version) { + checkRenderThreadState(); + mEGLContextClientVersion = version; + } + + /** + * Set the rendering mode. When renderMode is + * RENDERMODE_CONTINUOUSLY, the renderer is called + * repeatedly to re-render the scene. When renderMode + * is RENDERMODE_WHEN_DIRTY, the renderer only rendered when the surface + * is created, or when {@link #requestRender} is called. Defaults to RENDERMODE_CONTINUOUSLY. + *

+ * Using RENDERMODE_WHEN_DIRTY can improve battery life and overall system performance + * by allowing the GPU and CPU to idle when the view does not need to be updated. + *

+ * This method can only be called after {@link #setRenderer(Renderer)} + * + * @param renderMode one of the RENDERMODE_X constants + * @see #RENDERMODE_CONTINUOUSLY + * @see #RENDERMODE_WHEN_DIRTY + */ + public void setRenderMode(int renderMode) { + mGLThread.setRenderMode(renderMode); + } + + /** + * Get the current rendering mode. May be called + * from any thread. Must not be called before a renderer has been set. + * @return the current rendering mode. + * @see #RENDERMODE_CONTINUOUSLY + * @see #RENDERMODE_WHEN_DIRTY + */ + public int getRenderMode() { + return mGLThread.getRenderMode(); + } + + /** + * Request that the renderer render a frame. + * This method is typically used when the render mode has been set to + * {@link #RENDERMODE_WHEN_DIRTY}, so that frames are only rendered on demand. + * May be called + * from any thread. Must not be called before a renderer has been set. + */ + public void requestRender() { + mGLThread.requestRender(); + } + + /** + * use {@link #setRenderer} instead + */ + @Deprecated + @Override + public void setSurfaceTextureListener(SurfaceTextureListener listener) { + if (mRenderer == null) { + // 与普通TextureView一样 + super.setSurfaceTextureListener(listener); + } + Log.e(TAG, "setSurfaceTextureListener preserved, setRenderer() instead?"); + } + + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { + mGLThread.surfaceCreated(); + onSurfaceTextureSizeChanged(surface, width, height); + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { + mGLThread.onWindowResize(width, height); + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { + // Surface will be destroyed when we return + mGLThread.surfaceDestroyed(); + if(null != mRenderer) { + mRenderer.onSurfaceDestroyed(); + } + return true; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surface) { + + } + + /** + * Inform the view that the activity is paused. The owner of this view must + * call this method when the activity is paused. Calling this method will + * pause the rendering thread. + * Must not be called before a renderer has been set. + */ + public void onPause() { + mGLThread.onPause(); + } + + /** + * Inform the view that the activity is resumed. The owner of this view must + * call this method when the activity is resumed. Calling this method will + * recreate the OpenGL display and resume the rendering + * thread. + * Must not be called before a renderer has been set. + */ + public void onResume() { + mGLThread.onResume(); + } + + /** + * Queue a runnable to be run on the GL rendering thread. This can be used + * to communicate with the Renderer on the rendering thread. + * Must not be called before a renderer has been set. + * @param r the runnable to be run on the GL rendering thread. + */ + public void queueEvent(Runnable r) { + mGLThread.queueEvent(r); + } + + /** + * This method is used as part of the View class and is not normally + * called or subclassed by clients of GLTextureView. + */ + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (LOG_ATTACH_DETACH) { + Log.d(TAG, "onAttachedToWindow reattach =" + mDetached); + } + if (mDetached && (mRenderer != null)) { + int renderMode = RENDERMODE_CONTINUOUSLY; + if (mGLThread != null) { + renderMode = mGLThread.getRenderMode(); + } + mGLThread = new GLThread(mThisWeakRef); + if (renderMode != RENDERMODE_CONTINUOUSLY) { + mGLThread.setRenderMode(renderMode); + } + mGLThread.start(); + } + mDetached = false; + } + + @Override + protected void onDetachedFromWindow() { + if (LOG_ATTACH_DETACH) { + Log.d(TAG, "onDetachedFromWindow"); + } + if (mGLThread != null) { + mGLThread.requestExitAndWait(); + } + mDetached = true; + super.onDetachedFromWindow(); + } + + // ---------------------------------------------------------------------- + + /** + * An interface used to wrap a GL interface. + *

Typically + * used for implementing debugging and tracing on top of the default + * GL interface. You would typically use this by creating your own class + * that implemented all the GL methods by delegating to another GL instance. + * Then you could add your own behavior before or after calling the + * delegate. All the GLWrapper would do was instantiate and return the + * wrapper GL instance: + *

+     * class MyGLWrapper implements GLWrapper {
+     *     GL wrap(GL gl) {
+     *         return new MyGLImplementation(gl);
+     *     }
+     *     static class MyGLImplementation implements GL,GL10,GL11,... {
+     *         ...
+     *     }
+     * }
+     * 
+ * @see #setGLWrapper(GLWrapper) + */ + public interface GLWrapper { + /** + * Wraps a gl interface in another gl interface. + * @param gl a GL interface that is to be wrapped. + * @return either the input argument or another GL object that wraps the input argument. + */ + GL wrap(GL gl); + } + + /** + * A generic renderer interface. + *

+ * The renderer is responsible for making OpenGL calls to render a frame. + *

+ * GLTextureView clients typically create their own classes that implement + * this interface, and then call {@link GLTextureView#setRenderer} to + * register the renderer with the GLTextureView. + *

+ * + *

+ *

Developer Guides

+ *

For more information about how to use OpenGL, read the + * OpenGL developer guide.

+ *
+ * + *

Threading

+ * The renderer will be called on a separate thread, so that rendering + * performance is decoupled from the UI thread. Clients typically need to + * communicate with the renderer from the UI thread, because that's where + * input events are received. Clients can communicate using any of the + * standard Java techniques for cross-thread communication, or they can + * use the {@link GLTextureView#queueEvent(Runnable)} convenience method. + *

+ *

EGL Context Lost

+ * There are situations where the EGL rendering context will be lost. This + * typically happens when device wakes up after going to sleep. When + * the EGL context is lost, all OpenGL resources (such as textures) that are + * associated with that context will be automatically deleted. In order to + * keep rendering correctly, a renderer must recreate any lost resources + * that it still needs. The {@link #onSurfaceCreated(GL10, EGLConfig)} method + * is a convenient place to do this. + * + * + * @see #setRenderer(Renderer) + */ + public interface Renderer { + /** + * Called when the surface is created or recreated. + *

+ * Called when the rendering thread + * starts and whenever the EGL context is lost. The EGL context will typically + * be lost when the Android device awakes after going to sleep. + *

+ * Since this method is called at the beginning of rendering, as well as + * every time the EGL context is lost, this method is a convenient place to put + * code to create resources that need to be created when the rendering + * starts, and that need to be recreated when the EGL context is lost. + * Textures are an example of a resource that you might want to create + * here. + *

+ * Note that when the EGL context is lost, all OpenGL resources associated + * with that context will be automatically deleted. You do not need to call + * the corresponding "glDelete" methods such as glDeleteTextures to + * manually delete these lost resources. + *

+ * @param gl the GL interface. Use instanceof to + * test if the interface supports GL11 or higher interfaces. + * @param config the EGLConfig of the created surface. Can be used + * to create matching pbuffers. + */ + void onSurfaceCreated(GL10 gl, EGLConfig config); + + /** + * Called when the surface changed size. + *

+ * Called after the surface is created and whenever + * the OpenGL ES surface size changes. + *

+ * Typically you will set your viewport here. If your camera + * is fixed then you could also set your projection matrix here: + *

+         * void onSurfaceChanged(GL10 gl, int width, int height) {
+         *     gl.glViewport(0, 0, width, height);
+         *     // for a fixed camera, set the projection too
+         *     float ratio = (float) width / height;
+         *     gl.glMatrixMode(GL10.GL_PROJECTION);
+         *     gl.glLoadIdentity();
+         *     gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10);
+         * }
+         * 
+ * @param gl the GL interface. Use instanceof to + * test if the interface supports GL11 or higher interfaces. + * @param width + * @param height + */ + void onSurfaceChanged(GL10 gl, int width, int height); + + /** + * Called to draw the current frame. + *

+ * This method is responsible for drawing the current frame. + *

+ * The implementation of this method typically looks like this: + *

+         * void onDrawFrame(GL10 gl) {
+         *     gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
+         *     //... other gl calls to render the scene ...
+         * }
+         * 
+ * @param gl the GL interface. Use instanceof to + * test if the interface supports GL11 or higher interfaces. + */ + boolean onDrawFrame(GL10 gl); + + void onSurfaceDestroyed(); + } + + /** + * An interface for customizing the eglCreateContext and eglDestroyContext calls. + *

+ * This interface must be implemented by clients wishing to call + * {@link GLTextureView#setEGLContextFactory(EGLContextFactory)} + */ + public interface EGLContextFactory { + EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig); + void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context); + } + + private class DefaultContextFactory implements EGLContextFactory { + private int EGL_CONTEXT_CLIENT_VERSION = 0x3098; + + public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig config) { + int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, mEGLContextClientVersion, + EGL10.EGL_NONE }; + + return egl.eglCreateContext(display, config, EGL10.EGL_NO_CONTEXT, + mEGLContextClientVersion != 0 ? attrib_list : null); + } + + public void destroyContext(EGL10 egl, EGLDisplay display, + EGLContext context) { + if (!egl.eglDestroyContext(display, context)) { + Log.e("DefaultContextFactory", "display:" + display + " context: " + context); + if (LOG_THREADS) { + Log.i("DefaultContextFactory", "tid=" + Thread.currentThread().getId()); + } + EglHelper.throwEglException("eglDestroyContex", egl.eglGetError()); + } + } + } + + /** + * An interface for customizing the eglCreateWindowSurface and eglDestroySurface calls. + *

+ * This interface must be implemented by clients wishing to call + * {@link GLTextureView#setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory)} + */ + public interface EGLWindowSurfaceFactory { + /** + * @return null if the surface cannot be constructed. + */ + EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display, EGLConfig config, + Object nativeWindow); + void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface); + } + + private static class DefaultWindowSurfaceFactory implements EGLWindowSurfaceFactory { + + public EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display, + EGLConfig config, Object nativeWindow) { + EGLSurface result = null; + try { + result = egl.eglCreateWindowSurface(display, config, nativeWindow, null); + } catch (IllegalArgumentException e) { + // This exception indicates that the surface flinger surface + // is not valid. This can happen if the surface flinger surface has + // been torn down, but the application has not yet been + // notified via SurfaceHolder.Callback.surfaceDestroyed. + // In theory the application should be notified first, + // but in practice sometimes it is not. See b/4588890 + Log.e(TAG, "eglCreateWindowSurface", e); + } + return result; + } + + public void destroySurface(EGL10 egl, EGLDisplay display, + EGLSurface surface) { + egl.eglDestroySurface(display, surface); + } + } + + /** + * An interface for choosing an EGLConfig configuration from a list of + * potential configurations. + *

+ * This interface must be implemented by clients wishing to call + * {@link GLTextureView#setEGLConfigChooser(EGLConfigChooser)} + */ + public interface EGLConfigChooser { + /** + * Choose a configuration from the list. Implementors typically + * implement this method by calling + * {@link EGL10#eglChooseConfig} and iterating through the results. Please consult the + * EGL specification available from The Khronos Group to learn how to call eglChooseConfig. + * @param egl the EGL10 for the current display. + * @param display the current display. + * @return the chosen configuration. + */ + EGLConfig chooseConfig(EGL10 egl, EGLDisplay display); + } + + private abstract class BaseConfigChooser + implements EGLConfigChooser { + public BaseConfigChooser(int[] configSpec) { + mConfigSpec = filterConfigSpec(configSpec); + } + + public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { + int[] num_config = new int[1]; + if (!egl.eglChooseConfig(display, mConfigSpec, null, 0, + num_config)) { + throw new IllegalArgumentException("eglChooseConfig failed"); + } + + int numConfigs = num_config[0]; + + if (numConfigs <= 0) { + throw new IllegalArgumentException( + "No configs match configSpec"); + } + + EGLConfig[] configs = new EGLConfig[numConfigs]; + if (!egl.eglChooseConfig(display, mConfigSpec, configs, numConfigs, + num_config)) { + throw new IllegalArgumentException("eglChooseConfig#2 failed"); + } + EGLConfig config = chooseConfig(egl, display, configs); + if (config == null) { + throw new IllegalArgumentException("No config chosen"); + } + return config; + } + + abstract EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, + EGLConfig[] configs); + + protected int[] mConfigSpec; + + private int[] filterConfigSpec(int[] configSpec) { + if (mEGLContextClientVersion != 2 && mEGLContextClientVersion != 3) { + return configSpec; + } + /* We know none of the subclasses define EGL_RENDERABLE_TYPE. + * And we know the configSpec is well formed. + */ + int len = configSpec.length; + int[] newConfigSpec = new int[len + 2]; + System.arraycopy(configSpec, 0, newConfigSpec, 0, len-1); + newConfigSpec[len-1] = EGL10.EGL_RENDERABLE_TYPE; + if (mEGLContextClientVersion == 2) { + newConfigSpec[len] = EGL14.EGL_OPENGL_ES2_BIT; /* EGL_OPENGL_ES2_BIT */ + } else { + newConfigSpec[len] = EGLExt.EGL_OPENGL_ES3_BIT_KHR; /* EGL_OPENGL_ES3_BIT_KHR */ + } + newConfigSpec[len+1] = EGL10.EGL_NONE; + return newConfigSpec; + } + } + + /** + * Choose a configuration with exactly the specified r,g,b,a sizes, + * and at least the specified depth and stencil sizes. + */ + private class ComponentSizeChooser extends BaseConfigChooser { + public ComponentSizeChooser(int redSize, int greenSize, int blueSize, + int alphaSize, int depthSize, int stencilSize) { + super(new int[] { + EGL10.EGL_RED_SIZE, redSize, + EGL10.EGL_GREEN_SIZE, greenSize, + EGL10.EGL_BLUE_SIZE, blueSize, + EGL10.EGL_ALPHA_SIZE, alphaSize, + EGL10.EGL_DEPTH_SIZE, depthSize, + EGL10.EGL_STENCIL_SIZE, stencilSize, + EGL10.EGL_NONE}); + mValue = new int[1]; + mRedSize = redSize; + mGreenSize = greenSize; + mBlueSize = blueSize; + mAlphaSize = alphaSize; + mDepthSize = depthSize; + mStencilSize = stencilSize; + } + + @Override + public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, + EGLConfig[] configs) { + for (EGLConfig config : configs) { + int d = findConfigAttrib(egl, display, config, + EGL10.EGL_DEPTH_SIZE, 0); + int s = findConfigAttrib(egl, display, config, + EGL10.EGL_STENCIL_SIZE, 0); + if ((d >= mDepthSize) && (s >= mStencilSize)) { + int r = findConfigAttrib(egl, display, config, + EGL10.EGL_RED_SIZE, 0); + int g = findConfigAttrib(egl, display, config, + EGL10.EGL_GREEN_SIZE, 0); + int b = findConfigAttrib(egl, display, config, + EGL10.EGL_BLUE_SIZE, 0); + int a = findConfigAttrib(egl, display, config, + EGL10.EGL_ALPHA_SIZE, 0); + if ((r == mRedSize) && (g == mGreenSize) + && (b == mBlueSize) && (a == mAlphaSize)) { + return config; + } + } + } + return null; + } + + private int findConfigAttrib(EGL10 egl, EGLDisplay display, + EGLConfig config, int attribute, int defaultValue) { + + if (egl.eglGetConfigAttrib(display, config, attribute, mValue)) { + return mValue[0]; + } + return defaultValue; + } + + private int[] mValue; + // Subclasses can adjust these values: + protected int mRedSize; + protected int mGreenSize; + protected int mBlueSize; + protected int mAlphaSize; + protected int mDepthSize; + protected int mStencilSize; + } + + /** + * This class will choose a RGB_888 surface with + * or without a depth buffer. + * + */ + private class SimpleEGLConfigChooser extends ComponentSizeChooser { + public SimpleEGLConfigChooser(boolean withDepthBuffer) { + super(8, 8, 8, 0, withDepthBuffer ? 16 : 0, 0); + } + } + + /** + * An EGL helper class. + */ + + private static class EglHelper { + public EglHelper(WeakReference glTextureViewWeakRef) { + mGLTextureViewWeakRef = glTextureViewWeakRef; + } + + /** + * Initialize EGL for a given configuration spec. + */ + public void start() { + if (LOG_EGL) { + Log.w("EglHelper", "start() tid=" + Thread.currentThread().getId()); + } + /* + * Get an EGL instance + */ + mEgl = (EGL10) EGLContext.getEGL(); + + /* + * Get to the default display. + */ + mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); + + if (mEglDisplay == EGL10.EGL_NO_DISPLAY) { + throw new RuntimeException("eglGetDisplay failed"); + } + + /* + * We can now initialize EGL for that display + */ + int[] version = new int[2]; + if(!mEgl.eglInitialize(mEglDisplay, version)) { + throw new RuntimeException("eglInitialize failed"); + } + GLTextureView view = mGLTextureViewWeakRef.get(); + if (view == null) { + mEglConfig = null; + mEglContext = null; + } else { + mEglConfig = view.mEGLConfigChooser.chooseConfig(mEgl, mEglDisplay); + + /* + * Create an EGL context. We want to do this as rarely as we can, because an + * EGL context is a somewhat heavy object. + */ + mEglContext = view.mEGLContextFactory.createContext(mEgl, mEglDisplay, mEglConfig); + } + if (mEglContext == null || mEglContext == EGL10.EGL_NO_CONTEXT) { + mEglContext = null; + throwEglException("createContext"); + } + if (LOG_EGL) { + Log.w("EglHelper", "createContext " + mEglContext + " tid=" + Thread.currentThread().getId()); + } + + mEglSurface = null; + } + + /** + * Create an egl surface for the current SurfaceHolder surface. If a surface + * already exists, destroy it before creating the new surface. + * + * @return true if the surface was created successfully. + */ + public boolean createSurface() { + if (LOG_EGL) { + Log.w("EglHelper", "createSurface() tid=" + Thread.currentThread().getId()); + } + /* + * Check preconditions. + */ + if (mEgl == null) { + throw new RuntimeException("egl not initialized"); + } + if (mEglDisplay == null) { + throw new RuntimeException("eglDisplay not initialized"); + } + if (mEglConfig == null) { + throw new RuntimeException("mEglConfig not initialized"); + } + + /* + * The window size has changed, so we need to create a new + * surface. + */ + destroySurfaceImp(); + + /* + * Create an EGL surface we can render into. + */ + GLTextureView view = mGLTextureViewWeakRef.get(); + if (view != null) { + mEglSurface = view.mEGLWindowSurfaceFactory.createWindowSurface(mEgl, + mEglDisplay, mEglConfig, view.getSurfaceTexture()); + } else { + mEglSurface = null; + } + + if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) { + int error = mEgl.eglGetError(); + if (error == EGL10.EGL_BAD_NATIVE_WINDOW) { + Log.e("EglHelper", "createWindowSurface returned EGL_BAD_NATIVE_WINDOW."); + } + return false; + } + + /* + * Before we can issue GL commands, we need to make sure + * the context is current and bound to a surface. + */ + if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) { + /* + * Could not make the context current, probably because the underlying + * TextureView surface has been destroyed. + */ + logEglErrorAsWarning("EGLHelper", "eglMakeCurrent", mEgl.eglGetError()); + return false; + } + + return true; + } + + /** + * Create a GL object for the current EGL context. + * @return + */ + GL createGL() { + + GL gl = mEglContext.getGL(); + GLTextureView view = mGLTextureViewWeakRef.get(); + if (view != null) { + if (view.mGLWrapper != null) { + gl = view.mGLWrapper.wrap(gl); + } + + if ((view.mDebugFlags & (DEBUG_CHECK_GL_ERROR | DEBUG_LOG_GL_CALLS)) != 0) { + int configFlags = 0; + Writer log = null; + if ((view.mDebugFlags & DEBUG_CHECK_GL_ERROR) != 0) { + configFlags |= GLDebugHelper.CONFIG_CHECK_GL_ERROR; + } + if ((view.mDebugFlags & DEBUG_LOG_GL_CALLS) != 0) { + log = new LogWriter(); + } + gl = GLDebugHelper.wrap(gl, configFlags, log); + } + } + return gl; + } + + /** + * Display the current render surface. + * @return the EGL error code from eglSwapBuffers. + */ + public int swap() { + if (! mEgl.eglSwapBuffers(mEglDisplay, mEglSurface)) { + return mEgl.eglGetError(); + } + return EGL10.EGL_SUCCESS; + } + + public void destroySurface() { + if (LOG_EGL) { + Log.w("EglHelper", "destroySurface() tid=" + Thread.currentThread().getId()); + } + destroySurfaceImp(); + } + + private void destroySurfaceImp() { + if (mEglSurface != null && mEglSurface != EGL10.EGL_NO_SURFACE) { + mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE, + EGL10.EGL_NO_SURFACE, + EGL10.EGL_NO_CONTEXT); + GLTextureView view = mGLTextureViewWeakRef.get(); + if (view != null) { + view.mEGLWindowSurfaceFactory.destroySurface(mEgl, mEglDisplay, mEglSurface); + } + mEglSurface = null; + } + } + + public void finish() { + if (LOG_EGL) { + Log.w("EglHelper", "finish() tid=" + Thread.currentThread().getId()); + } + if (mEglContext != null) { + GLTextureView view = mGLTextureViewWeakRef.get(); + if (view != null) { + view.mEGLContextFactory.destroyContext(mEgl, mEglDisplay, mEglContext); + } + mEglContext = null; + } + if (mEglDisplay != null) { + mEgl.eglTerminate(mEglDisplay); + mEglDisplay = null; + } + } + + private void throwEglException(String function) { + throwEglException(function, mEgl.eglGetError()); + } + + public static void throwEglException(String function, int error) { + String message = formatEglError(function, error); + if (LOG_THREADS) { + Log.e("EglHelper", "throwEglException tid=" + Thread.currentThread().getId() + " " + + message); + } + throw new RuntimeException(message); + } + + public static void logEglErrorAsWarning(String tag, String function, int error) { + Log.w(tag, formatEglError(function, error)); + } + + public static String formatEglError(String function, int error) { + return function + " failed: " + eglGetErrorString(error); + } + + static String eglGetErrorString(int error) { + switch (error) { + case EGL11.EGL_SUCCESS: + return "EGL_SUCCESS"; + case EGL11.EGL_NOT_INITIALIZED: + return "EGL_NOT_INITIALIZED"; + case EGL11.EGL_BAD_ACCESS: + return "EGL_BAD_ACCESS"; + case EGL11.EGL_BAD_ALLOC: + return "EGL_BAD_ALLOC"; + case EGL11.EGL_BAD_ATTRIBUTE: + return "EGL_BAD_ATTRIBUTE"; + case EGL11.EGL_BAD_CONFIG: + return "EGL_BAD_CONFIG"; + case EGL11.EGL_BAD_CONTEXT: + return "EGL_BAD_CONTEXT"; + case EGL11.EGL_BAD_CURRENT_SURFACE: + return "EGL_BAD_CURRENT_SURFACE"; + case EGL11.EGL_BAD_DISPLAY: + return "EGL_BAD_DISPLAY"; + case EGL11.EGL_BAD_MATCH: + return "EGL_BAD_MATCH"; + case EGL11.EGL_BAD_NATIVE_PIXMAP: + return "EGL_BAD_NATIVE_PIXMAP"; + case EGL11.EGL_BAD_NATIVE_WINDOW: + return "EGL_BAD_NATIVE_WINDOW"; + case EGL11.EGL_BAD_PARAMETER: + return "EGL_BAD_PARAMETER"; + case EGL11.EGL_BAD_SURFACE: + return "EGL_BAD_SURFACE"; + case EGL11.EGL_CONTEXT_LOST: + return "EGL_CONTEXT_LOST"; + default: + return "0x" + Integer.toHexString(error); + } + } + + private WeakReference mGLTextureViewWeakRef; + EGL10 mEgl; + EGLDisplay mEglDisplay; + EGLSurface mEglSurface; + EGLConfig mEglConfig; + EGLContext mEglContext; + + } + + /** + * A generic GL Thread. Takes care of initializing EGL and GL. Delegates + * to a Renderer instance to do the actual drawing. Can be configured to + * render continuously or on request. + * + * All potentially blocking synchronization is done through the + * sGLThreadManager object. This avoids multiple-lock ordering issues. + * + */ + static class GLThread extends Thread { + GLThread(WeakReference glTextureViewWeakRef) { + super(); + mWidth = 0; + mHeight = 0; + mRequestRender = true; + mRenderMode = RENDERMODE_CONTINUOUSLY; + mGLTextureViewWeakRef = glTextureViewWeakRef; + } + + @Override + public void run() { + setName("GLThread " + getId()); + if (LOG_THREADS) { + Log.i("GLThread", "starting tid=" + getId()); + } + + try { + guardedRun(); + } catch (InterruptedException e) { + // fall thru and exit normally + } finally { + sGLThreadManager.threadExiting(this); + } + } + + /* + * This private method should only be called inside a + * synchronized(sGLThreadManager) block. + */ + private void stopEglSurfaceLocked() { + if (mHaveEglSurface) { + mHaveEglSurface = false; + mEglHelper.destroySurface(); + } + } + + /* + * This private method should only be called inside a + * synchronized(sGLThreadManager) block. + */ + private void stopEglContextLocked() { + if (mHaveEglContext) { + mEglHelper.finish(); + mHaveEglContext = false; + sGLThreadManager.releaseEglContextLocked(this); + } + } + private void guardedRun() throws InterruptedException { + mEglHelper = new EglHelper(mGLTextureViewWeakRef); + mHaveEglContext = false; + mHaveEglSurface = false; + try { + GL10 gl = null; + boolean createEglContext = false; + boolean createEglSurface = false; + boolean createGlInterface = false; + boolean lostEglContext = false; + boolean sizeChanged = false; + boolean wantRenderNotification = false; + boolean doRenderNotification = false; + boolean askedToReleaseEglContext = false; + int w = 0; + int h = 0; + Runnable event = null; + + while (true) { + synchronized (sGLThreadManager) { + while (true) { + if (mShouldExit) { + return; + } + + if (! mEventQueue.isEmpty()) { + event = mEventQueue.remove(0); + break; + } + + // Update the pause state. + boolean pausing = false; + if (mPaused != mRequestPaused) { + pausing = mRequestPaused; + mPaused = mRequestPaused; + sGLThreadManager.notifyAll(); + if (LOG_PAUSE_RESUME) { + Log.i("GLThread", "mPaused is now " + mPaused + " tid=" + getId()); + } + } + + // Do we need to give up the EGL context? + if (mShouldReleaseEglContext) { + if (LOG_SURFACE) { + Log.i("GLThread", "releasing EGL context because asked to tid=" + getId()); + } + stopEglSurfaceLocked(); + stopEglContextLocked(); + mShouldReleaseEglContext = false; + askedToReleaseEglContext = true; + } + + // Have we lost the EGL context? + if (lostEglContext) { + stopEglSurfaceLocked(); + stopEglContextLocked(); + lostEglContext = false; + } + + // When pausing, release the EGL surface: + if (pausing && mHaveEglSurface) { + if (LOG_SURFACE) { + Log.i("GLThread", "releasing EGL surface because paused tid=" + getId()); + } + stopEglSurfaceLocked(); + } + + // When pausing, optionally release the EGL Context: + if (pausing && mHaveEglContext) { + GLTextureView view = mGLTextureViewWeakRef.get(); + boolean preserveEglContextOnPause = view == null ? + false : view.mPreserveEGLContextOnPause; + if (!preserveEglContextOnPause || sGLThreadManager.shouldReleaseEGLContextWhenPausing()) { + stopEglContextLocked(); + if (LOG_SURFACE) { + Log.i("GLThread", "releasing EGL context because paused tid=" + getId()); + } + } + } + + // When pausing, optionally terminate EGL: + if (pausing) { + if (sGLThreadManager.shouldTerminateEGLWhenPausing()) { + mEglHelper.finish(); + if (LOG_SURFACE) { + Log.i("GLThread", "terminating EGL because paused tid=" + getId()); + } + } + } + + // Have we lost the TextureView surface? + if ((! mHasSurface) && (! mWaitingForSurface)) { + if (LOG_SURFACE) { + Log.i("GLThread", "noticed TextureView surface lost tid=" + getId()); + } + if (mHaveEglSurface) { + stopEglSurfaceLocked(); + } + mWaitingForSurface = true; + mSurfaceIsBad = false; + sGLThreadManager.notifyAll(); + } + + // Have we acquired the surface view surface? + if (mHasSurface && mWaitingForSurface) { + if (LOG_SURFACE) { + Log.i("GLThread", "noticed TextureView surface acquired tid=" + getId()); + } + mWaitingForSurface = false; + sGLThreadManager.notifyAll(); + } + + if (doRenderNotification) { + if (LOG_SURFACE) { + Log.i("GLThread", "sending render notification tid=" + getId()); + } + wantRenderNotification = false; + doRenderNotification = false; + mRenderComplete = true; + sGLThreadManager.notifyAll(); + } + + // Ready to draw? + if (readyToDraw()) { + + // If we don't have an EGL context, try to acquire one. + if (! mHaveEglContext) { + if (askedToReleaseEglContext) { + askedToReleaseEglContext = false; + } else if (sGLThreadManager.tryAcquireEglContextLocked(this)) { + try { + mEglHelper.start(); + } catch (RuntimeException t) { + sGLThreadManager.releaseEglContextLocked(this); + throw t; + } + mHaveEglContext = true; + createEglContext = true; + + sGLThreadManager.notifyAll(); + } + } + + if (mHaveEglContext && !mHaveEglSurface) { + mHaveEglSurface = true; + createEglSurface = true; + createGlInterface = true; + sizeChanged = true; + } + + if (mHaveEglSurface) { + if (mSizeChanged) { + sizeChanged = true; + w = mWidth; + h = mHeight; + wantRenderNotification = true; + if (LOG_SURFACE) { + Log.i("GLThread", + "noticing that we want render notification tid=" + + getId()); + } + + // Destroy and recreate the EGL surface. + createEglSurface = true; + + mSizeChanged = false; + } + mRequestRender = false; + sGLThreadManager.notifyAll(); + break; + } + } + + // By design, this is the only place in a GLThread thread where we wait(). + if (LOG_THREADS) { + Log.i("GLThread", "waiting tid=" + getId() + + " mHaveEglContext: " + mHaveEglContext + + " mHaveEglSurface: " + mHaveEglSurface + + " mFinishedCreatingEglSurface: " + mFinishedCreatingEglSurface + + " mPaused: " + mPaused + + " mHasSurface: " + mHasSurface + + " mSurfaceIsBad: " + mSurfaceIsBad + + " mWaitingForSurface: " + mWaitingForSurface + + " mWidth: " + mWidth + + " mHeight: " + mHeight + + " mRequestRender: " + mRequestRender + + " mRenderMode: " + mRenderMode); + } + sGLThreadManager.wait(); + } + } // end of synchronized(sGLThreadManager) + + if (event != null) { + event.run(); + event = null; + continue; + } + + if (createEglSurface) { + if (LOG_SURFACE) { + Log.w("GLThread", "egl createSurface"); + } + if (mEglHelper.createSurface()) { + synchronized(sGLThreadManager) { + mFinishedCreatingEglSurface = true; + sGLThreadManager.notifyAll(); + } + } else { + synchronized(sGLThreadManager) { + mFinishedCreatingEglSurface = true; + mSurfaceIsBad = true; + sGLThreadManager.notifyAll(); + } + continue; + } + createEglSurface = false; + } + + if (createGlInterface) { + gl = (GL10) mEglHelper.createGL(); + + sGLThreadManager.checkGLDriver(gl); + createGlInterface = false; + } + + if (createEglContext) { + if (LOG_RENDERER) { + Log.w("GLThread", "onSurfaceCreated"); + } + GLTextureView view = mGLTextureViewWeakRef.get(); + if (view != null) { + view.mRenderer.onSurfaceCreated(gl, mEglHelper.mEglConfig); + } + createEglContext = false; + } + + if (sizeChanged) { + if (LOG_RENDERER) { + Log.w("GLThread", "onSurfaceChanged(" + w + ", " + h + ")"); + } + GLTextureView view = mGLTextureViewWeakRef.get(); + if (view != null) { + view.mRenderer.onSurfaceChanged(gl, w, h); + } + sizeChanged = false; + } + + if (LOG_RENDERER_DRAW_FRAME) { + Log.w("GLThread", "onDrawFrame tid=" + getId()); + } + boolean needSwap = false; + GLTextureView view = mGLTextureViewWeakRef.get(); + if (view != null) { + needSwap = view.mRenderer.onDrawFrame(gl); + } + if(needSwap) { + int swapError = mEglHelper.swap(); + switch (swapError) { + case EGL10.EGL_SUCCESS: + break; + case EGL11.EGL_CONTEXT_LOST: + if (LOG_SURFACE) { + Log.i("GLThread", "egl context lost tid=" + getId()); + } + lostEglContext = true; + break; + default: + // Other errors typically mean that the current surface is bad, + // probably because the TextureView surface has been destroyed, + // but we haven't been notified yet. + // Log the error to help developers understand why rendering stopped. + EglHelper.logEglErrorAsWarning("GLThread", "eglSwapBuffers", swapError); + + synchronized (sGLThreadManager) { + mSurfaceIsBad = true; + sGLThreadManager.notifyAll(); + } + break; + } + } + + if (wantRenderNotification) { + doRenderNotification = true; + } + } + + } finally { + /* + * clean-up everything... + */ + synchronized (sGLThreadManager) { + stopEglSurfaceLocked(); + stopEglContextLocked(); + } + } + } + + public boolean ableToDraw() { + return mHaveEglContext && mHaveEglSurface && readyToDraw(); + } + + private boolean readyToDraw() { + return (!mPaused) && mHasSurface && (!mSurfaceIsBad) + && (mWidth > 0) && (mHeight > 0) + && (mRequestRender || (mRenderMode == RENDERMODE_CONTINUOUSLY)); + } + + public void setRenderMode(int renderMode) { + if ( !((RENDERMODE_WHEN_DIRTY <= renderMode) && (renderMode <= RENDERMODE_CONTINUOUSLY)) ) { + throw new IllegalArgumentException("renderMode"); + } + synchronized(sGLThreadManager) { + mRenderMode = renderMode; + sGLThreadManager.notifyAll(); + } + } + + public int getRenderMode() { + synchronized(sGLThreadManager) { + return mRenderMode; + } + } + + public void requestRender() { + synchronized(sGLThreadManager) { + mRequestRender = true; + sGLThreadManager.notifyAll(); + } + } + + public void surfaceCreated() { + synchronized(sGLThreadManager) { + if (LOG_THREADS) { + Log.i("GLThread", "surfaceCreated tid=" + getId()); + } + mHasSurface = true; + mFinishedCreatingEglSurface = false; + sGLThreadManager.notifyAll(); + while (mWaitingForSurface + && !mFinishedCreatingEglSurface + && !mExited) { + try { + sGLThreadManager.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void surfaceDestroyed() { + synchronized(sGLThreadManager) { + if (LOG_THREADS) { + Log.i("GLThread", "surfaceDestroyed tid=" + getId()); + } + mHasSurface = false; + sGLThreadManager.notifyAll(); + while((!mWaitingForSurface) && (!mExited)) { + try { + sGLThreadManager.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void onPause() { + synchronized (sGLThreadManager) { + if (LOG_PAUSE_RESUME) { + Log.i("GLThread", "onPause tid=" + getId()); + } + mRequestPaused = true; + sGLThreadManager.notifyAll(); + while ((! mExited) && (! mPaused)) { + if (LOG_PAUSE_RESUME) { + Log.i("Main thread", "onPause waiting for mPaused."); + } + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void onResume() { + synchronized (sGLThreadManager) { + if (LOG_PAUSE_RESUME) { + Log.i("GLThread", "onResume tid=" + getId()); + } + mRequestPaused = false; + mRequestRender = true; + mRenderComplete = false; + sGLThreadManager.notifyAll(); + while ((! mExited) && mPaused && (!mRenderComplete)) { + if (LOG_PAUSE_RESUME) { + Log.i("Main thread", "onResume waiting for !mPaused."); + } + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void onWindowResize(int w, int h) { + synchronized (sGLThreadManager) { + mWidth = w; + mHeight = h; + mSizeChanged = true; + mRequestRender = true; + mRenderComplete = false; + sGLThreadManager.notifyAll(); + + // Wait for thread to react to resize and render a frame + while (! mExited && !mPaused && !mRenderComplete + && ableToDraw()) { + if (LOG_SURFACE) { + Log.i("Main thread", "onWindowResize waiting for render complete from tid=" + getId()); + } + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void requestExitAndWait() { + // don't call this from GLThread thread or it is a guaranteed + // deadlock! + synchronized(sGLThreadManager) { + mShouldExit = true; + sGLThreadManager.notifyAll(); + while (! mExited) { + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void requestReleaseEglContextLocked() { + mShouldReleaseEglContext = true; + sGLThreadManager.notifyAll(); + } + + /** + * Queue an "event" to be run on the GL rendering thread. + * @param r the runnable to be run on the GL rendering thread. + */ + public void queueEvent(Runnable r) { + if (r == null) { + throw new IllegalArgumentException("r must not be null"); + } + synchronized(sGLThreadManager) { + mEventQueue.add(r); + sGLThreadManager.notifyAll(); + } + } + + // Once the thread is started, all accesses to the following member + // variables are protected by the sGLThreadManager monitor + private boolean mShouldExit; + private boolean mExited; + private boolean mRequestPaused; + private boolean mPaused; + private boolean mHasSurface; + private boolean mSurfaceIsBad; + private boolean mWaitingForSurface; + private boolean mHaveEglContext; + private boolean mHaveEglSurface; + private boolean mFinishedCreatingEglSurface; + private boolean mShouldReleaseEglContext; + private int mWidth; + private int mHeight; + private int mRenderMode; + private boolean mRequestRender; + private boolean mRenderComplete; + private ArrayList mEventQueue = new ArrayList(); + private boolean mSizeChanged = true; + + // End of member variables protected by the sGLThreadManager monitor. + + private EglHelper mEglHelper; + + /** + * Set once at thread construction time, nulled out when the parent view is garbage + * called. This weak reference allows the GLTextureView to be garbage collected while + * the GLThread is still alive. + */ + private WeakReference mGLTextureViewWeakRef; + + } + + static class LogWriter extends Writer { + + @Override public void close() { + flushBuilder(); + } + + @Override public void flush() { + flushBuilder(); + } + + @Override public void write(char[] buf, int offset, int count) { + for(int i = 0; i < count; i++) { + char c = buf[offset + i]; + if ( c == '\n') { + flushBuilder(); + } + else { + mBuilder.append(c); + } + } + } + + private void flushBuilder() { + if (mBuilder.length() > 0) { + Log.v("GLTextureView", mBuilder.toString()); + mBuilder.delete(0, mBuilder.length()); + } + } + + private StringBuilder mBuilder = new StringBuilder(); + } + + + private void checkRenderThreadState() { + if (mGLThread != null) { + throw new IllegalStateException( + "setRenderer has already been called for this instance."); + } + } + + private static class GLThreadManager { + private static String TAG = "GLThreadManager"; + + public synchronized void threadExiting(GLThread thread) { + if (LOG_THREADS) { + Log.i("GLThread", "exiting tid=" + thread.getId()); + } + thread.mExited = true; + if (mEglOwner == thread) { + mEglOwner = null; + } + notifyAll(); + } + + /* + * Tries once to acquire the right to use an EGL + * context. Does not block. Requires that we are already + * in the sGLThreadManager monitor when this is called. + * + * @return true if the right to use an EGL context was acquired. + */ + public boolean tryAcquireEglContextLocked(GLThread thread) { + if (mEglOwner == thread || mEglOwner == null) { + mEglOwner = thread; + notifyAll(); + return true; + } + checkGLESVersion(); + if (mMultipleGLESContextsAllowed) { + return true; + } + // Notify the owning thread that it should release the context. + // TODO: implement a fairness policy. Currently + // if the owning thread is drawing continuously it will just + // reacquire the EGL context. + if (mEglOwner != null) { + mEglOwner.requestReleaseEglContextLocked(); + } + return false; + } + + /* + * Releases the EGL context. Requires that we are already in the + * sGLThreadManager monitor when this is called. + */ + public void releaseEglContextLocked(GLThread thread) { + if (mEglOwner == thread) { + mEglOwner = null; + } + notifyAll(); + } + + public synchronized boolean shouldReleaseEGLContextWhenPausing() { + // Release the EGL context when pausing even if + // the hardware supports multiple EGL contexts. + // Otherwise the device could run out of EGL contexts. + return mLimitedGLESContexts; + } + + public synchronized boolean shouldTerminateEGLWhenPausing() { + checkGLESVersion(); + return !mMultipleGLESContextsAllowed; + } + + public synchronized void checkGLDriver(GL10 gl) { + if (! mGLESDriverCheckComplete) { + checkGLESVersion(); + String renderer = gl.glGetString(GL10.GL_RENDERER); + if (mGLESVersion < kGLES_20) { + mMultipleGLESContextsAllowed = + ! renderer.startsWith(kMSM7K_RENDERER_PREFIX); + notifyAll(); + } + mLimitedGLESContexts = !mMultipleGLESContextsAllowed; + if (LOG_SURFACE) { + Log.w(TAG, "checkGLDriver renderer = \"" + renderer + "\" multipleContextsAllowed = " + + mMultipleGLESContextsAllowed + + " mLimitedGLESContexts = " + mLimitedGLESContexts); + } + mGLESDriverCheckComplete = true; + } + } + + private void checkGLESVersion() { + if (! mGLESVersionCheckComplete) { +// mGLESVersion = minus.android.support.opengl.SystemProperties.getInt( +// "ro.opengles.version", +// ConfigurationInfo.GL_ES_VERSION_UNDEFINED); +// if (mGLESVersion >= kGLES_20) { + mMultipleGLESContextsAllowed = true; +// } + if (LOG_SURFACE) { + Log.w(TAG, "checkGLESVersion mGLESVersion =" + + " " + mGLESVersion + " mMultipleGLESContextsAllowed = " + mMultipleGLESContextsAllowed); + } + mGLESVersionCheckComplete = true; + } + } + + /** + * This check was required for some pre-Android-3.0 hardware. Android 3.0 provides + * support for hardware-accelerated views, therefore multiple EGL contexts are + * supported on all Android 3.0+ EGL drivers. + */ + private boolean mGLESVersionCheckComplete; + private int mGLESVersion; + private boolean mGLESDriverCheckComplete; + private boolean mMultipleGLESContextsAllowed; + private boolean mLimitedGLESContexts; + private static final int kGLES_20 = 0x20000; + private static final String kMSM7K_RENDERER_PREFIX = + "Q3Dimension MSM7500 "; + private GLThread mEglOwner; + } + + private static final GLThreadManager sGLThreadManager = new GLThreadManager(); + + private final WeakReference mThisWeakRef = + new WeakReference(this); + private GLThread mGLThread; + private Renderer mRenderer; + private boolean mDetached; + private EGLConfigChooser mEGLConfigChooser; + private EGLContextFactory mEGLContextFactory; + private EGLWindowSurfaceFactory mEGLWindowSurfaceFactory; + private GLWrapper mGLWrapper; + private int mDebugFlags; + private int mEGLContextClientVersion; + private boolean mPreserveEGLContextOnPause; +} +``` \ No newline at end of file diff --git "a/VideoDevelopment/OpenGL/4.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" "b/VideoDevelopment/OpenGL/4.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" new file mode 100644 index 00000000..5532ef7d --- /dev/null +++ "b/VideoDevelopment/OpenGL/4.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" @@ -0,0 +1,863 @@ +## OpenGL Es绘制三角形 + +OpenGL ES的绘制需要有一下步骤: + +- 顶点输入 + + 开始绘制图形之前,我们必须先给OpenGL输入一些顶点数据。OpenGL是一个3D图形库,所以我们在OpenGL中所指定的所有坐标都是3D坐标(xyz)。OpenGL并不是简单地把所有的3D坐标变换成屏幕上的2D像素。OpenGL仅当3D坐标在3个轴(xyz)上都为-1.0到1.0的范围内采取处理它。所有在所谓的标准化设备坐标范围内的坐标才会最终呈现到屏幕上(在这个范围以外的坐标都不会显示)。 + +- 顶点着色器 + + ```glsl + #version 330 core + layout (location = 0) in vec3 position; + + void main() + { + gl_Position = vec4(position.x, position.y, position.z, 1.0); + } + ``` + + 每个着色器都起始于一个版本声明。OpenGL 3.3以及和更高版本中,GLSL版本号和OpenGL的版本是匹配的(比如说GLSL 420版本对应于OpenGL 4.2)。我们同样明确表示我们会使用核心模式。 + + 下一步,使用`in`关键字,在顶点着色器中声明所有的输入顶点属性(Input Vertex Attribute)。现在我们只关心位置(Position)数据,所以我们只需要一个顶点属性。GLSL有一个向量数据类型,它包含1到4个`float`分量,包含的数量可以从它的后缀数字看出来。由于每个顶点都有一个3D坐标,我们就创建一个`vec3`输入变量position。我们同样也通过`layout (location = 0)`设定了输入变量的位置值(Location)你后面会看到为什么我们会需要这个位置值。 + + 为了设置顶点着色器的输出,我们必须把位置数据赋值给预定义的gl_Position变量,它在幕后是`vec4`类型的。在main函数的最后,我们将gl_Position设置的值会成为该顶点着色器的输出。由于我们的输入是一个3分量的向量,我们必须把它转换为4分量的。我们可以把`vec3`的数据作为`vec4`构造器的参数,同时把`w`分量设置为`1.0f`(我们会在后面解释为什么)来完成这一任务。 + +- 编译着色器 + + 写完顶点着色器后,为了能让OpenGL使用它,我们必须在运行时动态编译它。 + +- 片段着色器 + + 片断着色器全是关于计算你的像素最后的颜色输出。颜色使用RGBA。 + + ```glsl + #version 330 core + + out vec4 color; + + void main() + { + color = vec4(1.0f, 0.5f, 0.2f, 1.0f); + } + ``` + + 片段着色器只需要一个输出变量,这个变量是一个4分量向量,它表示的是最终的输出颜色,我们应该自己将其计算出来。我们可以用`out`关键字声明输出变量,这里我们命名为color。下面,我们将一个alpha值为1.0(1.0代表完全不透明)的橘黄色的`vec4`赋值给颜色输出。之后也是需要编译着色器。 + +- 着色器程序(Shader Program Object) + + 着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。如果要使用刚才编译的着色器我们必须把它们链接为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。 + + 当链接着色器至一个程序的时候,它会把每个着色器的输出链接到下个着色器的输入。当输出和输入不匹配的时候,你会得到一个连接错误。我们需要把之前编译的着色器附加到程序队形上,然后使用glLinkProgram链接他们: + + ```java + glAttachShader(shaderProgram, vertexShader); + glAttachShader(shaderProgram, fragmentShader); + glLinkProgram(shaderProgram); + ``` + + 链接完后需要使用glUseProgram方法,用刚创建的程序对象作为参数,以激活这个程序对象。 + +- 链接顶点属性 + + 顶点着色器允许我们指定任何以顶点属性为形式的输入。这使其具有很强的灵活性的同时,它还的确意味着我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。所以,我们必须在渲染前指定OpenGL该如何解释顶点数据。 + + 我们的顶点缓冲数据会被解析成下面的样子: + + ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/vertex_attribute_pointer.png) + + 有了这些信息我们就可以使用glVertexAttribPointer函数告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上)了: + + ``` + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0); + glEnableVertexAttribArray(0); + ``` + + glVertexAttribPointer函数的参数非常多,所以我会逐一介绍它们: + + - 第一个参数指定我们要配置的顶点属性。还记得我们在顶点着色器中使用`layout(location = 0)`定义了position顶点属性的位置值(Location)吗?它可以把顶点属性的位置值设置为`0`。因为我们希望把数据传递到这一个顶点属性中,所以这里我们传入`0`。 + - 第二个参数指定顶点属性的大小。顶点属性是一个`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。我们会在后面详细解释这个参数。 + + + +下面需要实现GLSurfaceView.Render接口,实现要绘制的部分: + +- 写顶点着色器和片段着色器文件。 +- 加载编译顶点着色器和片段着色器。 +- 确定需要绘制图形的坐标和颜色数据。 +- 创建program对象,连接顶点和片断着色器,将坐标数据、颜色数据传到OpenGL ES程序中。] +- 设置视图窗口(viewport)。 +- 使颜色缓冲区的内容显示到屏幕上。 + + + +### GLSL配置 + +编写着色器需要用到GLSL,但是在Studio中默认是不支持关键字高亮和智能提示的,所以需要先安装插件。 + +Preferences -> Plugins -> 搜GLSL Support安装就可以了。 + + + +### 编写着色器 + +安装完GLSL插件后,就可以开始了。一般将GLSL文件放到raw或assets目录,我们这里在raw目录上右键New,然后选择GLSL Shader创建就可以了,创建后默认会生成一个main()函数。 + +- 顶点着色器(vertex_simple_shade.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; + } + ``` + +- 片段着色器(fragment_simple_shade.glsl) + + ```glsl + // 声明着色器的版本 + #version 300 es + // 声明着色器中浮点变量的默认精度 + precision mediump float; + // 声明一个输入名为vColor的4分向量,来自上面的顶点着色器 + in vec4 vColor; + // 声明一个4分向量的输出变量fragColor + out vec4 fragColor; + void main() { + // 将输入的颜色值数据拷贝到fragColor变量中,输出到颜色缓冲区 + fragColor = vColor; + } + ``` + + + +### 编写GLSurfaceView.Render类 + +主要有以下功能: + +1. 声明绘制图形的坐标和颜色数据 +2. 为顶点位置及颜色申请本地内存 +3. 加载编译顶点着色器和片段着色器 +4. 创建program,连接顶点和片段着色器并链接program +5. 设置窗口大小 +6. 完成绘制 + +```java +public class MainActivity extends AppCompatActivity { + private GLSurfaceView mGlSurfaceView; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mGlSurfaceView = new MyGLSurfaceView(this); + setContentView(mGlSurfaceView); + } +} + +class MyGLSurfaceView extends GLSurfaceView { + private final MyGLRenderer renderer; + + public MyGLSurfaceView(Context context) { + super(context); + setEGLContextClientVersion(3); + renderer = new MyGLRenderer(); + setRenderer(renderer); + } +} + +class MyGLRenderer implements GLSurfaceView.Renderer { + //一个Float占用4Byte + private static final int BYTES_PER_FLOAT = 4; + //三个顶点 + private static final int POSITION_COMPONENT_COUNT = 3; + //顶点位置缓存 + private final FloatBuffer vertexBuffer; + //顶点颜色缓存 + private final FloatBuffer colorBuffer; + //渲染程序 + private int mProgram; + + /*****************1.声明绘制图形的坐标和颜色数据 start**************/ + //三个顶点的位置参数 + private float triangleCoords[] = { + 0.5f, 0.5f, 0.0f, // top + -0.5f, -0.5f, 0.0f, // bottom left + 0.5f, -0.5f, 0.0f // bottom right + }; + + //三个顶点的颜色参数 + private float color[] = { + 1.0f, 0.0f, 0.0f, 1.0f,// top + 0.0f, 1.0f, 0.0f, 1.0f,// bottom left + 0.0f, 0.0f, 1.0f, 1.0f// bottom right + }; + /*****************1.声明绘制图形的坐标和颜色数据 end**************/ + public MyGLRenderer() { + /****************2.为顶点位置及颜色申请本地内存 start************/ + //顶点位置相关 + //分配本地内存空间,每个浮点型占4字节空间;将坐标数据转换为FloatBuffer,用以传入给OpenGL ES程序 + vertexBuffer = ByteBuffer.allocateDirect(triangleCoords.length * BYTES_PER_FLOAT) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + vertexBuffer.put(triangleCoords); + // 将数组数据put进buffer之后,指针并不是在首位,所以一定要position到0,至关重要!否则会有很多奇妙的错误!将缓冲区的指针移动到头部,保证数据是从最开始处读取 + vertexBuffer.position(0); + + //顶点颜色相关 + colorBuffer = ByteBuffer.allocateDirect(color.length * BYTES_PER_FLOAT) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + colorBuffer.put(color); + colorBuffer.position(0); + /****************2.为顶点位置及颜色申请本地内存 end************/ + } + + + public void onSurfaceCreated(GL10 unused, EGLConfig config) { + //将背景设置为白色 + GLES30.glClearColor(1.0f, 1.0f, 1.0f, 1.0f); + + /******************3.加载编译顶点着色器和片段着色器 start**********/ + //编译顶点着色程序 + String vertexShaderStr = readResource(MyApplication.getInstance(), R.raw.vertex_simple_shade); + int vertexShaderId = compileVertexShader(vertexShaderStr); + //编译片段着色程序 + String fragmentShaderStr = readResource(MyApplication.getInstance(), R.raw.fragment_simple_shade); + int fragmentShaderId = compileFragmentShader(fragmentShaderStr); + /******************3.加载编译顶点着色器和片段着色器 end**********/ + /******************4.创建program,连接顶点和片段着色器并链接program start***********/ + //连接程序 + mProgram = linkProgram(vertexShaderId, fragmentShaderId); + /******************4.创建program,连接顶点和片段着色器并链接program end***********/ + //在OpenGLES环境中使用程序 + GLES30.glUseProgram(mProgram); + } + public void onSurfaceChanged(GL10 unused, int width, int height) { + /*********5.设置绘制窗口********/ + GLES30.glViewport(0, 0, width, height); + } + public void onDrawFrame(GL10 unused) { + /**********6.绘制************/ + //把颜色缓冲区设置为我们预设的颜色 + GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT); + + //绑定vertex坐标数据,告诉OpenGL可以在缓冲区vertextBuffer中获取数据 + GLES30.glVertexAttribPointer(0, 3, GLES30.GL_FLOAT, false, 0, vertexBuffer); + //启用顶点位置句柄 + GLES30.glEnableVertexAttribArray(0); + + //准备颜色数据 + /** + * 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); + //启用顶点颜色句柄 + GLES30.glEnableVertexAttribArray(1); + + //绘制三个点 +// GLES30.glDrawArrays(GLES30.GL_POINTS, 0, POSITION_COMPONENT_COUNT); + + //绘制三条线 +// GLES30.glLineWidth(3);//设置线宽 +// GLES30.glDrawArrays(GLES30.GL_LINE_LOOP, 0, POSITION_COMPONENT_COUNT); + + //绘制三角形 + GLES30.glDrawArrays(GLES30.GL_TRIANGLES, 0, POSITION_COMPONENT_COUNT); + + //禁止顶点数组的句柄 + GLES30.glDisableVertexAttribArray(0); + GLES30.glDisableVertexAttribArray(1); + } + /** + * 编译顶点着色器 + * + * @param shaderCode + */ + public static int compileVertexShader(String shaderCode) { + return compileShader(GLES30.GL_VERTEX_SHADER, shaderCode); + } + + /** + * 编译片段着色器 + * + * @param shaderCode + */ + public static int compileFragmentShader(String shaderCode) { + return compileShader(GLES30.GL_FRAGMENT_SHADER, shaderCode); + } + + /** + * 加载并编译着色器代码 + * + * @param type 顶点着色器:GLES30.GL_VERTEX_SHADER + * 片段着色器:GLES30.GL_FRAGMENT_SHADER + * @param shaderCode + */ + private static int compileShader(int type, String shaderCode) { + //传入渲染器类型参数的type,创建一个对应的着色器对象 + final int shaderId = GLES30.glCreateShader(type); + if (shaderId != 0) { + // 传入着色器对象和字符串shaderCode定义的源代码,将二者关联起来 + GLES30.glShaderSource(shaderId, shaderCode); + // 传入着色器对象,并对其进行编译 + GLES30.glCompileShader(shaderId); + //检测状态 + final int[] compileStatus = new int[1]; + GLES30.glGetShaderiv(shaderId, GLES30.GL_COMPILE_STATUS, compileStatus, 0); + if (compileStatus[0] == 0) { + String logInfo = GLES30.glGetShaderInfoLog(shaderId); + System.err.println(logInfo); + //创建失败 + GLES30.glDeleteShader(shaderId); + return 0; + } + return shaderId; + } else { + //创建失败 + return 0; + } + } + + /** + * 链接小程序 + * + * @param vertexShaderId 顶点着色器 + * @param fragmentShaderId 片段着色器 + */ + public static int linkProgram(int vertexShaderId, int fragmentShaderId) { + //创建一个空的OpenGLES程序 + final int programId = GLES30.glCreateProgram(); + if (programId != 0) { + //将顶点着色器加入到程序 + GLES30.glAttachShader(programId, vertexShaderId); + //将片元着色器加入到程序中 + GLES30.glAttachShader(programId, fragmentShaderId); + //链接着色器程序 + GLES30.glLinkProgram(programId); + final int[] linkStatus = new int[1]; + + GLES30.glGetProgramiv(programId, GLES30.GL_LINK_STATUS, linkStatus, 0); + if (linkStatus[0] == 0) { + String logInfo = GLES30.glGetProgramInfoLog(programId); + System.err.println(logInfo); + GLES30.glDeleteProgram(programId); + return 0; + } + return programId; + } else { + //创建失败 + return 0; + } + } + + /** + * 验证程序片段是否有效 + * + * @param programObjectId + */ + public static boolean validProgram(int programObjectId) { + GLES30.glValidateProgram(programObjectId); + final int[] programStatus = new int[1]; + GLES30.glGetProgramiv(programObjectId, GLES30.GL_VALIDATE_STATUS, programStatus, 0); + return programStatus[0] != 0; + } + + /** + * 读取资源 + * + * @param resourceId + */ + public static String readResource(Context context, int resourceId) { + StringBuilder builder = new StringBuilder(); + try { + InputStream inputStream = context.getApplicationContext().getResources().openRawResource(resourceId); + InputStreamReader streamReader = new InputStreamReader(inputStream); + + BufferedReader bufferedReader = new BufferedReader(streamReader); + String textLine; + while ((textLine = bufferedReader.readLine()) != null) { + builder.append(textLine); + builder.append("\n"); + } + } catch (IOException e) { + e.printStackTrace(); + } catch (Resources.NotFoundException e) { + e.printStackTrace(); + } + return builder.toString(); + } +} +``` + +效果如下: + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_es_tri.jpg) + +我们设置的是数据来看,应该是等腰三角形,但是实际效果并不是,这是因为前面说到的OpenGL ES使用的是虚拟坐标导致的。如果想让绘制一个等腰三角形该怎么做呢? + +### 变成等腰三角形的原理 + +打个比方现在屏幕的宽高比是1:2,那上面的话的三角形的高度就是宽度的2,我们只需要通过换算把高度变成现在的高度 * 1/2 就可以得到等腰三角形了。 + +这里就牵扯到了要对坐标向量进行换算,这里的换算需要使用矩阵来进行。总体分为两部分: + +- 或者获得一个矩阵,可以把坐标范围从【-2,2】换算成【-1,1】的范围内。(提供了Matrix.orthoM来处理矩阵) +- 如何将这个矩阵传递给GLSL中。(与获取顶点索引类似,可以在GLSL中声明一个mat4类型的矩阵变量,获取其索引,再传递值给它) + + + +### 向量(Vector) + +具有大小和方向的量。它可以形象化的表示为带箭头的线段。箭头代表方向、长度代表大小。在GLSL中一个向量有最多4个分量,每个分量值都代表空间中的一个坐标,它们可以通过vec.x、vec.y、vec.z和vec.w来获取。vec.w分量不是用作表达空间中的位置(因为我们处理的是3D不是4D),而是用在所谓透视划分上。 + +### 矩阵 + +由m*n个数按照一定顺序排列成m行n列的矩形数表称为矩阵,而向量则是由n个有序数组成的数组。 + +所以矩阵中的每一个行可以看做一个行向量,每一列也可以看成一个列向量,所以说向量是矩阵的一部分。 + +在三维图形学中,一般使用的是4阶矩阵。在DirectX中使用的是行向量,如[xyzw],所以与矩阵相乘时,向量在前矩阵在后。OpenGL中使用的是列向量,如[xyzx]T,所以与矩阵相乘时,矩阵在前,向量在后,最终通过变换矩阵得到想要的向量。 + + + +### 相机 + +这里的相机指的是观察外界的视角并不是我们生活中的相机。这里的相机是指我们从照相机或者摄像机的角度来观察这个世界。 相机对应于OpenGL的世界,决定相机拍摄的结果(最终屏幕上展示的结果),包括相机位置、相机观察方向以及相机的UP方向。 + +1. 相机位置:相机在3D空间里面的坐标点。 +2. 相机观察方向:相机镜头的朝向,朝前拍、朝后拍、朝左拍、朝右拍。 +3. 相机UP方向:相机顶端志祥的方向,例如斜着拿、反着拿。 + +Android OpenGL ES程序中,我们可以通过Matrix.setLookAtm来对相机进行设置: + +```java +/** +* Defines a viewing transformation in terms of an eye point, a center of +* view, and an up vector. +* +* @param rm returns the result +* @param rmOffset index into rm where the result matrix starts +* @param eyeX eye point X +* @param eyeY eye point Y +* @param eyeZ eye point Z +* @param centerX center of view X +* @param centerY center of view Y +* @param centerZ center of view Z +* @param upX up vector X +* @param upY up vector Y +* @param upZ up vector Z +*/ +public static void setLookAtM(float[] rm, //接收相机变换矩阵 +int rmOffset, //变换矩阵的起始位置(偏移量) +float eyeX,float eyeY, float eyeZ, //相机位置 +float centerX,float centerY,float centerZ, //观测点位置 +float upX,float upY,float upZ) //up向量在xyz上的分量) { + +------------省略代码------------- +} +``` + + + +### 投影 + +相机视角观察到的世界最终要变成平面2D图像展示到屏幕上,这个过程就是投影,从3D到2D的转换。 + +Android OpenGL ES的投影分为两种: + +- 正交投影 + + 物体呈现出来的大小不会随着其距离视点的远近而发生变化。通过Matrix.orthoM()来设置正交投影。 + + ```java + /** + * Computes an orthographic projection matrix. + * + * @param m returns the result + * @param mOffset + * @param left + * @param right + * @param bottom + * @param top + * @param near + * @param far + */ + public static void orthoM(float[] m, //接收正交投影的变换矩阵 + int mOffset, //变换矩阵的起始位置(偏移量) + float left, //相对观察点近面的左边距 + float right,//相对观察点近面的右边距 + float bottom, //相对观察点近面的下边距 + float top,//相对观察点近面的上边距 + float near,//相对观察点近面距离 + float far) //相对观察点远面距离{ + ---------------省略代码-------------- + } + ``` + + + +- 透视投影 + + 物体离视点越远,呈现出来的越小。离视点越近,呈现出来的越大。通过Matrix.frustumM()来设置透明投影: + + ```java + /** + * Defines a projection matrix in terms of six clip planes. + * + * @param m the float array that holds the output perspective matrix + * @param offset the offset into float array m where the perspective + * matrix data is written + * @param left + * @param right + * @param bottom + * @param top + * @param near + * @param far + */ + public static void frustumM(float[] m, //接收透视投影的变换矩阵 + int mOffset, //变换矩阵的起始位置(偏移量) + float left,//相对观察点近面的左边距 + float right,//相对观察点近面的右边距 + float bottom, //相对观察点近面的下边距 + float top, //相对观察点近面的上边距 + float near, //相对观察点近面距离 + float far) //相对观察点远面距离 { + ---------------省略代码-------------- + } + ``` + + + +### 变换矩阵 + +在OpenGL ES中顶点位置信息的表示都是使用的向量,如下每一行是一个顶点的位置向量(x,y,z)。想要使三角形显示为等腰三角形,就需要在虚拟坐标系中完成对各个顶点位置的变换,也就是对三个向量的变换。而想要实现对向量的变换就要用到变换矩阵。 + +```java +//三个顶点的位置参数 +private float triangleCoords[] = { + 0.5f, 0.5f, 0.0f, // top + -0.5f, -0.5f, 0.0f, // bottom left + 0.5f, -0.5f, 0.0f // bottom right +}; +``` + +变换矩阵需要透过相应的相机和投影的操作才能得到,具体方法如下: + +```java +Matrix.multiplyMM (float[] result, //接收相乘结果 + int resultOffset, //接收矩阵的起始位置(偏移量) + float[] lhs, //左矩阵 + int lhsOffset, //左矩阵的起始位置(偏移量) + float[] rhs, //右矩阵 + int rhsOffset) //右矩阵的起始位置(偏移量) +``` + + + +也就是说为了解决坐标中宽高不一样的问题,我们可以应用OpenGL正确的比例下通过投影模式和相机视图坐标转换图形对象来完成。为了应用投影和相机视图,我们创建一个投影矩阵和一个相机视图矩阵,并把他们应用于OpenGL渲染管道中,投影矩阵重新计算你的图形的坐标,使他们正确的映射到Android设备的屏幕,相机视图矩阵创建一个转换,它将从一个特定的位置显示对象。 + +### 绘制等腰三角形 + +​ 在上面绘制三角形的基础上进行修改。 + +### 工具类 + +将用到的着色器功能以及资源读取glsl的功能封装成工具类: + +```java +public class ShaderUtils { + private static final String TAG = "ShaderUtils"; + /** + * 编译顶点着色器 + * @param shaderCode + */ + public static int compileVertexShader(String shaderCode) { + return compileShader(GLES30.GL_VERTEX_SHADER, shaderCode); + } + + /** + * 编译片段着色器 + * @param shaderCode + */ + public static int compileFragmentShader(String shaderCode) { + return compileShader(GLES30.GL_FRAGMENT_SHADER, shaderCode); + } + + /** + * 编译 + * @param type 顶点着色器:GLES30.GL_VERTEX_SHADER + * 片段着色器:GLES30.GL_FRAGMENT_SHADER + * @param shaderCode + */ + private static int compileShader(int type, String shaderCode) { + //创建一个着色器 + final int shaderId = GLES30.glCreateShader(type); + if (shaderId != 0) { + GLES30.glShaderSource(shaderId, shaderCode); + GLES30.glCompileShader(shaderId); + //检测状态 + final int[] compileStatus = new int[1]; + GLES30.glGetShaderiv(shaderId, GLES30.GL_COMPILE_STATUS, compileStatus, 0); + if (compileStatus[0] == 0) { + String logInfo = GLES30.glGetShaderInfoLog(shaderId); + System.err.println(logInfo); + //创建失败 + GLES30.glDeleteShader(shaderId); + return 0; + } + return shaderId; + } else { + //创建失败 + return 0; + } + } + + /** + * 链接小程序 + * @param vertexShaderId 顶点着色器 + * @param fragmentShaderId 片段着色器 + */ + public static int linkProgram(int vertexShaderId, int fragmentShaderId) { + //创建一个空的OpenGLES程序 + final int programId = GLES30.glCreateProgram(); + if (programId != 0) { + //将顶点着色器加入到程序 + GLES30.glAttachShader(programId, vertexShaderId); + //将片元着色器加入到程序中 + GLES30.glAttachShader(programId, fragmentShaderId); + //链接着色器程序 + GLES30.glLinkProgram(programId); + final int[] linkStatus = new int[1]; + + GLES30.glGetProgramiv(programId, GLES30.GL_LINK_STATUS, linkStatus, 0); + if (linkStatus[0] == 0) { + String logInfo = GLES30.glGetProgramInfoLog(programId); + System.err.println(logInfo); + GLES30.glDeleteProgram(programId); + return 0; + } + return programId; + } else { + //创建失败 + return 0; + } + } + + /** + * 验证程序片段是否有效 + * @param programObjectId + */ + public static boolean validProgram(int programObjectId) { + GLES30.glValidateProgram(programObjectId); + final int[] programStatus = new int[1]; + GLES30.glGetProgramiv(programObjectId, GLES30.GL_VALIDATE_STATUS, programStatus, 0); + return programStatus[0] != 0; + } +} +``` + + + +```java +public class ResReadUtils { + + /** + * 读取资源 + * @param resourceId + */ + public static String readResource(int resourceId) { + StringBuilder builder = new StringBuilder(); + try { + InputStream inputStream = MyApplication.getInstance().getResources().openRawResource(resourceId); + InputStreamReader streamReader = new InputStreamReader(inputStream); + + BufferedReader bufferedReader = new BufferedReader(streamReader); + String textLine; + while ((textLine = bufferedReader.readLine()) != null) { + builder.append(textLine); + builder.append("\n"); + } + } catch (IOException e) { + e.printStackTrace(); + } catch (Resources.NotFoundException e) { + e.printStackTrace(); + } + return builder.toString(); + } +} +``` + + + +- 修改顶点着色器,增加矩阵变换(修改vertex_simple_shade.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; + // 变换矩阵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; + } + ``` + +- 在GLSurfaceView.Render实现类中定义矩阵变量 + +- 把变换矩阵设置给顶点渲染器 + +其他所有代码都和上面的一样,只是在Renderer的实现类中增加对转换矩阵的部分 + +```java +class TriangleRender implements GLSurfaceView.Renderer { + //一个Float占用4Byte + private static final int BYTES_PER_FLOAT = 4; + //三个顶点 + private static final int POSITION_COMPONENT_COUNT = 3; + //顶点位置缓存 + private final FloatBuffer vertexBuffer; + //顶点颜色缓存 + private final FloatBuffer colorBuffer; + //渲染程序 + private int mProgram; + private int uMatrixLocation; + // 矩阵数组 + private final float[] mProjectionMatrix = new float[]{ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1, + }; + //三个顶点的位置参数 + private float triangleCoords[] = { + 0.5f, 0.5f, 0.0f, // top + -0.5f, -0.5f, 0.0f, // bottom left + 0.5f, -0.5f, 0.0f // bottom right + }; + + //三个顶点的颜色参数 + private float color[] = { + 1.0f, 0.0f, 0.0f, 1.0f,// top + 0.0f, 1.0f, 0.0f, 1.0f,// bottom left + 0.0f, 0.0f, 1.0f, 1.0f// bottom right + }; + + public TriangleRender() { + //顶点位置相关 + //分配本地内存空间,每个浮点型占4字节空间;将坐标数据转换为FloatBuffer,用以传入给OpenGL ES程序 + vertexBuffer = ByteBuffer.allocateDirect(triangleCoords.length * BYTES_PER_FLOAT) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + vertexBuffer.put(triangleCoords); + vertexBuffer.position(0); + + //顶点颜色相关 + colorBuffer = ByteBuffer.allocateDirect(color.length * BYTES_PER_FLOAT) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + colorBuffer.put(color); + colorBuffer.position(0); + } + + @Override + public void onSurfaceCreated(GL10 gl, EGLConfig config) { + //将背景设置为白色 + GLES30.glClearColor(1.0f, 1.0f, 1.0f, 1.0f); + //编译顶点着色程序 + String vertexShaderStr = readResource(R.raw.vertex_simple_shade); + int vertexShaderId = compileVertexShader(vertexShaderStr); + //编译片段着色程序 + String fragmentShaderStr = readResource(R.raw.fragment_simple_shade); + int fragmentShaderId = compileFragmentShader(fragmentShaderStr); + //连接程序 + mProgram = linkProgram(vertexShaderId, fragmentShaderId); + //在OpenGLES环境中使用程序 + GLES30.glUseProgram(mProgram); + + /**********新加的部分,获取变换矩阵以及其的位置、颜色等************/ + uMatrixLocation = GLES30.glGetUniformLocation(mProgram, "u_Matrix"); + } + + @Override + public void onSurfaceChanged(GL10 gl, int width, int height) { + GLES30.glViewport(0, 0, width, height); + /**********新加的部分,将变换矩阵传入顶点渲染器************/ + //计算宽高比 + // 边长比(>=1),非宽高比 + float aspectRatio = width > height ? + (float) width / (float) height : + (float) height / (float) width; + + // 1. 矩阵数组 + // 2. 结果矩阵起始的偏移量 + // 3. left:x的最小值 + // 4. right:x的最大值 + // 5. bottom:y的最小值 + // 6. top:y的最大值 + // 7. near:z的最小值 + // 8. far:z的最大值 + if (width > height) { + // 横屏 + Matrix.orthoM(mProjectionMatrix, 0, -aspectRatio, aspectRatio, -1f, 1f, -1f, 1f); + } else { + // 竖屏or正方形 + Matrix.orthoM(mProjectionMatrix, 0, -1f, 1f, -aspectRatio, aspectRatio, -1f, 1f); + } + // 更新u_Matrix的值,即更新矩阵数组 + GLES30.glUniformMatrix4fv(uMatrixLocation, 1, false, mProjectionMatrix, 0); + } + + @Override + public void onDrawFrame(GL10 gl) { + //把颜色缓冲区设置为我们预设的颜色 + GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT); + //绑定vertex坐标数据,告诉OpenGL可以在缓冲区vertexBuffer中获取vPosition的护具 + GLES30.glVertexAttribPointer(0, 3, GLES30.GL_FLOAT, false, 0, vertexBuffer); + //启用顶点位置句柄 + GLES30.glEnableVertexAttribArray(0); + //准备颜色数据 + GLES30.glVertexAttribPointer(1, 4, GLES30.GL_FLOAT, false, 0, colorBuffer); + //启用顶点颜色句柄 + GLES30.glEnableVertexAttribArray(1); + //绘制三角形 + GLES30.glDrawArrays(GLES30.GL_TRIANGLES, 0, POSITION_COMPONENT_COUNT); + //禁止顶点数组的句柄 + GLES30.glDisableVertexAttribArray(0); + GLES30.glDisableVertexAttribArray(1); + } +} +``` + diff --git "a/VideoDevelopment/OpenGL/5.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/5.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" new file mode 100644 index 00000000..581c449c --- /dev/null +++ "b/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" @@ -0,0 +1,511 @@ +## 5.OpenGL ES绘制矩形及圆形 + + + +### 顶点法和索引法 + +- 顶点法 + + 上一篇文章中写的绘制点、线、三角形都是使用GLES30.glDrawArrays()来绘制,它是顶点法。根据传入的顶点顺序进行绘制。顶点复用情况少,可读性低。 + +- 索引法 + + 根据索引序列,在顶点序列中找到对应的顶点,并根据绘制的方式,组成相应的图元绘制,用的是GLES30.glDrawElements(),称为索引法。相对于顶点法在复杂图形的绘制中无法避免大量顶点重复的情况,索引法可以减少很多重复顶点占用的空间,所以复杂的图形下推荐使用索引法。顶点复用情况多,客读性高。 + +之前说过OpenGL ES提供的的图元单位是三角形,想要绘制其他多边形,就要利用三角形来拼成。 矩形是两个三角形,而圆形则是由很多个三角形组成,个数越多,圆越圆。 + + + +### 绘制矩形 + + + +- 顶点着色器与上一个三角形的一样 + +- 片段着色器与上一个三角形的一样 + +- Render的实现如下 + + 顶点法: + + ```java + package com.charon.opengldemo.rectangle; + + import android.opengl.GLES20; + import android.opengl.GLES30; + import android.opengl.GLSurfaceView; + import android.opengl.Matrix; + + import com.charon.opengldemo.R; + import com.charon.opengldemo.util.ResReadUtils; + import com.charon.opengldemo.util.ShaderUtils; + + import java.nio.ByteBuffer; + import java.nio.ByteOrder; + import java.nio.FloatBuffer; + import java.nio.ShortBuffer; + + import javax.microedition.khronos.egl.EGLConfig; + import javax.microedition.khronos.opengles.GL10; + + public class RectangleRender implements GLSurfaceView.Renderer { + //一个Float占用4Byte + private static final int BYTES_PER_FLOAT = 4; + //顶点位置缓存 + private final FloatBuffer vertexBuffer; + //顶点颜色缓存 + private final FloatBuffer colorBuffer; + //渲染程序 + private int mProgram; + + //返回属性变量的位置 + //变换矩阵 + private int uMatrixLocation; + //位置 + private int aPositionLocation; + //颜色 + private int aColorLocation; + + /** + * 坐标占用的向量个数 + */ + private static final int POSITION_COMPONENT_COUNT = 2; + private static final float[] POINT_DATA = { + -0.5f, -0.5f, + 0.5f, -0.5f, + -0.5f, 0.5f, + 0.5f, 0.5f, + }; + /** + * 颜色占用的向量个数 + */ + private static final int COLOR_COMPONENT_COUNT = 4; + private static final float[] COLOR_DATA = { + // 一个顶点有3个向量数据:r、g、b、a + 1f, 0.5f, 0.5f, 0f, + 1f, 0f, 1f, 0f, + 0f, 1f, 1f, 0f, + 1f, 1f, 0f, 0f + }; + private final float[] mProjectionMatrix = new float[16]; + + public RectangleRender() { + //顶点位置相关 + //分配本地内存空间,每个浮点型占4字节空间;将坐标数据转换为FloatBuffer,用以传入给OpenGL ES程序 + vertexBuffer = ByteBuffer.allocateDirect(POINT_DATA.length * BYTES_PER_FLOAT) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + vertexBuffer.put(POINT_DATA); + vertexBuffer.position(0); + + //顶点颜色相关 + colorBuffer = ByteBuffer.allocateDirect(COLOR_DATA.length * BYTES_PER_FLOAT) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + colorBuffer.put(COLOR_DATA); + colorBuffer.position(0); + } + + @Override + public void onSurfaceCreated(GL10 gl, EGLConfig config) { + //将背景设置为白色 + GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f); + + //编译顶点着色程序 + String vertexShaderStr = ResReadUtils.readResource(R.raw.vertex_simple_shade); + int vertexShaderId = ShaderUtils.compileVertexShader(vertexShaderStr); + //编译片段着色程序 + String fragmentShaderStr = ResReadUtils.readResource(R.raw.fragment_simple_shade); + int fragmentShaderId = ShaderUtils.compileFragmentShader(fragmentShaderStr); + //连接程序 + mProgram = ShaderUtils.linkProgram(vertexShaderId, fragmentShaderId); + //在OpenGLES环境中使用程序 + GLES30.glUseProgram(mProgram); + + + uMatrixLocation = GLES30.glGetUniformLocation(mProgram, "u_Matrix"); + aPositionLocation = GLES30.glGetAttribLocation(mProgram, "vPosition"); + aColorLocation = GLES30.glGetAttribLocation(mProgram, "aColor"); + } + + @Override + public void onSurfaceChanged(GL10 gl, int width, int height) { + //设置绘制窗口 + GLES30.glViewport(0, 0, width, height); + //正交投影方式 + final float aspectRatio = width > height ? + (float) width / (float) height : + (float) height / (float) width; + if (width > height) { + //横屏 + Matrix.orthoM(mProjectionMatrix, 0, -aspectRatio, aspectRatio, -1f, 1f, -1f, 1f); + } else { + //竖屏 + Matrix.orthoM(mProjectionMatrix, 0, -1f, 1f, -aspectRatio, aspectRatio, -1f, 1f); + } + } + + @Override + public void onDrawFrame(GL10 gl) { + //把颜色缓冲区设置为我们预设的颜色 + GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT); + + //将变换矩阵传入顶点渲染器 + GLES20.glUniformMatrix4fv(uMatrixLocation, 1, false, mProjectionMatrix, 0); + //准备坐标数据 + GLES30.glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GLES30.GL_FLOAT, false, 0, vertexBuffer); + //启用顶点位置句柄 + GLES30.glEnableVertexAttribArray(aPositionLocation); + + //准备颜色数据 + GLES30.glVertexAttribPointer(aColorLocation, COLOR_COMPONENT_COUNT, GLES30.GL_FLOAT, false, 0, colorBuffer); + //启用顶点颜色句柄 + GLES30.glEnableVertexAttribArray(aColorLocation); + // 开始绘制 + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, POINT_DATA.length / POSITION_COMPONENT_COUNT); + //禁止顶点数组的句柄 + GLES30.glDisableVertexAttribArray(aPositionLocation); + GLES30.glDisableVertexAttribArray(aColorLocation); + } + } + ``` + + 索引法: + + ```java + public class RectangleRender implements GLSurfaceView.Renderer { + //一个Float占用4Byte + private static final int BYTES_PER_FLOAT = 4; + //顶点个数 + private static final int POSITION_COMPONENT_COUNT = 4; + //顶点位置缓存 + private final FloatBuffer vertexBuffer; + //顶点颜色缓存 + private final FloatBuffer colorBuffer; + //顶点索引缓存 + private final ShortBuffer indicesBuffer; + //渲染程序 + private int mProgram; + + //相机矩阵 + private final float[] mViewMatrix = new float[16]; + //投影矩阵 + private final float[] mProjectMatrix = new float[16]; + //最终变换矩阵 + private final float[] mMVPMatrix = new float[16]; + + //返回属性变量的位置 + //变换矩阵 + private int uMatrixLocation; + //位置 + private int aPositionLocation; + //颜色 + private int aColorLocation; + + //四个顶点的位置参数 + private float rectangleCoords[] = { + -0.5f, 0.5f, 0.0f,//top left + -0.5f, -0.5f, 0.0f, // bottom left + 0.5f, -0.5f, 0.0f, // bottom right + 0.5f, 0.5f, 0.0f // top right + }; + + /** + * 顶点索引 + */ + private short[] indices = { + 0, 1, 2, 0, 2, 3 + }; + + //四个顶点的颜色参数 + private float color[] = { + 0.0f, 0.0f, 1.0f, 1.0f,//top left + 0.0f, 1.0f, 0.0f, 1.0f,// bottom left + 0.0f, 0.0f, 1.0f, 1.0f,// bottom right + 1.0f, 0.0f, 0.0f, 1.0f// top right + }; + + public RectangleRender() { + //顶点位置相关 + //分配本地内存空间,每个浮点型占4字节空间;将坐标数据转换为FloatBuffer,用以传入给OpenGL ES程序 + vertexBuffer = ByteBuffer.allocateDirect(rectangleCoords.length * BYTES_PER_FLOAT) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + vertexBuffer.put(rectangleCoords); + vertexBuffer.position(0); + + //顶点颜色相关 + colorBuffer = ByteBuffer.allocateDirect(color.length * BYTES_PER_FLOAT) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + colorBuffer.put(color); + colorBuffer.position(0); + + //顶点索引相关 + indicesBuffer = ByteBuffer.allocateDirect(indices.length * 4) + .order(ByteOrder.nativeOrder()) + .asShortBuffer(); + indicesBuffer.put(indices); + indicesBuffer.position(0); + } + + @Override + public void onSurfaceCreated(GL10 gl, EGLConfig config) { + //将背景设置为白色 + GLES20.glClearColor(1.0f,1.0f,1.0f,1.0f); + + //编译顶点着色程序 + String vertexShaderStr = ResReadUtils.readResource(R.raw.vertex_simple_shade); + int vertexShaderId = ShaderUtils.compileVertexShader(vertexShaderStr); + //编译片段着色程序 + String fragmentShaderStr = ResReadUtils.readResource(R.raw.fragment_simple_shade); + int fragmentShaderId = ShaderUtils.compileFragmentShader(fragmentShaderStr); + //连接程序 + mProgram = ShaderUtils.linkProgram(vertexShaderId, fragmentShaderId); + //在OpenGLES环境中使用程序 + GLES30.glUseProgram(mProgram); + + + uMatrixLocation = GLES30.glGetUniformLocation(mProgram, "u_Matrix"); + aPositionLocation = GLES30.glGetAttribLocation(mProgram, "vPosition"); + aColorLocation = GLES30.glGetAttribLocation(mProgram, "aColor"); + } + + @Override + public void onSurfaceChanged(GL10 gl, int width, int height) { + //设置绘制窗口 + GLES30.glViewport(0, 0, width, height); + + + //相机和透视投影方式 + //计算宽高比 + float ratio=(float)width/height; + //设置透视投影 + Matrix.frustumM(mProjectMatrix, 0, -ratio, ratio, -1, 1, 3, 7); + //设置相机位置 + Matrix.setLookAtM(mViewMatrix, 0, 0, 0, 7.0f, 0f, 0f, 0f, 0f, 1.0f, 0.0f); + //计算变换矩阵 + Matrix.multiplyMM(mMVPMatrix,0,mProjectMatrix,0,mViewMatrix,0); + + + /*//正交投影方式 + final float aspectRatio = width > height ? + (float) width / (float) height : + (float) height / (float) width; + if (width > height) { + //横屏 + Matrix.orthoM(mMVPMatrix, 0, -aspectRatio, aspectRatio, -1f, 1f, -1f, 1f); + } else { + //竖屏 + Matrix.orthoM(mMVPMatrix, 0, -1f, 1f, -aspectRatio, aspectRatio, -1f, 1f); + }*/ + } + + @Override + public void onDrawFrame(GL10 gl) { + //把颜色缓冲区设置为我们预设的颜色 + GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT); + + //将变换矩阵传入顶点渲染器 + GLES20.glUniformMatrix4fv(uMatrixLocation,1,false,mMVPMatrix,0); + //准备坐标数据 + GLES30.glVertexAttribPointer(aPositionLocation, 3, GLES30.GL_FLOAT, false, 0, vertexBuffer); + //启用顶点位置句柄 + GLES30.glEnableVertexAttribArray(aPositionLocation); + + //准备颜色数据 + GLES30.glVertexAttribPointer(aColorLocation, 4, GLES30.GL_FLOAT, false, 0, colorBuffer); + //启用顶点颜色句柄 + GLES30.glEnableVertexAttribArray(aColorLocation); + + //绘制三角形 + GLES30.glDrawElements(GL10.GL_TRIANGLES, indices.length, GL10.GL_UNSIGNED_SHORT, indicesBuffer); + + //禁止顶点数组的句柄 + GLES30.glDisableVertexAttribArray(aPositionLocation); + GLES30.glDisableVertexAttribArray(aColorLocation); + } + } + ``` + + + +### 绘制圆形 + +其他也都和上面的一样,只有Render不同,如下: + +```java +public class CircularRender implements GLSurfaceView.Renderer { + //一个Float占用4Byte + private static final int BYTES_PER_FLOAT = 4; + //顶点位置缓存 + private final FloatBuffer vertexBuffer; + //顶点颜色缓存 + private final FloatBuffer colorBuffer; + //渲染程序 + private int mProgram; + + //相机矩阵 + private final float[] mViewMatrix = new float[16]; + //投影矩阵 + private final float[] mProjectMatrix = new float[16]; + //最终变换矩阵 + private final float[] mMVPMatrix = new float[16]; + + //返回属性变量的位置 + //变换矩阵 + private int uMatrixLocation; + //位置 + private int aPositionLocation; + //颜色 + private int aColorLocation; + + //圆形顶点位置 + private float circularCoords[]; + //顶点的颜色 + private float color[]; + + + public CircularRender() { + createPositions(1,60); + + //顶点位置相关 + //分配本地内存空间,每个浮点型占4字节空间;将坐标数据转换为FloatBuffer,用以传入给OpenGL ES程序 + vertexBuffer = ByteBuffer.allocateDirect(circularCoords.length * BYTES_PER_FLOAT) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + vertexBuffer.put(circularCoords); + vertexBuffer.position(0); + + //顶点颜色相关 + colorBuffer = ByteBuffer.allocateDirect(color.length * BYTES_PER_FLOAT) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + colorBuffer.put(color); + colorBuffer.position(0); + } + + private void createPositions(int radius, int n){ + ArrayList data=new ArrayList<>(); + data.add(0.0f); //设置圆心坐标 + data.add(0.0f); + data.add(0.0f); + float angDegSpan=360f/n; + for(float i=0;i<360+angDegSpan;i+=angDegSpan){ + data.add((float) (radius*Math.sin(i*Math.PI/180f))); + data.add((float)(radius*Math.cos(i*Math.PI/180f))); + data.add(0.0f); + } + float[] f=new float[data.size()]; + for (int i=0;i tempC = new ArrayList<>(); + ArrayList totalC = new ArrayList<>(); + tempC.add(1.0f); + tempC.add(0.0f); + tempC.add(0.0f); + tempC.add(1.0f); + for (int i=0;i height ? + (float) width / (float) height : + (float) height / (float) width; + if (width > height) { + //横屏 + Matrix.orthoM(mMVPMatrix, 0, -aspectRatio, aspectRatio, -1f, 1f, -1f, 1f); + } else { + //竖屏 + Matrix.orthoM(mMVPMatrix, 0, -1f, 1f, -aspectRatio, aspectRatio, -1f, 1f); + }*/ + } + + @Override + public void onDrawFrame(GL10 gl) { + //把颜色缓冲区设置为我们预设的颜色 + GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT); + + //将变换矩阵传入顶点渲染器 + GLES20.glUniformMatrix4fv(uMatrixLocation,1,false,mMVPMatrix,0); + //准备坐标数据 + GLES30.glVertexAttribPointer(aPositionLocation, 3, GLES30.GL_FLOAT, false, 0, vertexBuffer); + //启用顶点位置句柄 + GLES30.glEnableVertexAttribArray(aPositionLocation); + + //准备颜色数据 + GLES30.glVertexAttribPointer(aColorLocation, 4, GLES30.GL_FLOAT, false, 0, colorBuffer); + //启用顶点颜色句柄 + GLES30.glEnableVertexAttribArray(aColorLocation); + + //绘制圆形 + GLES30.glDrawArrays(GLES30.GL_TRIANGLE_FAN, 0, circularCoords.length/3); + + //禁止顶点数组的句柄 + GLES30.glDisableVertexAttribArray(aPositionLocation); + GLES30.glDisableVertexAttribArray(aColorLocation); + } +} +``` + + + + + + + + + + + diff --git "a/VideoDevelopment/OpenGL/6.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" "b/VideoDevelopment/OpenGL/6.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" new file mode 100644 index 00000000..a286783e --- /dev/null +++ "b/VideoDevelopment/OpenGL/6.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" @@ -0,0 +1,232 @@ +## 6.OpenGL ES着色器语言GLSL + +顶点着色器: + +``` +#version 330 core +layout (location = 0) in vec3 position; // position变量的属性位置值为0 + +out vec4 vertexColor; // 为片段着色器指定一个颜色输出 + +void main() +{ + gl_Position = vec4(position, 1.0); // 注意我们如何把一个vec3作为vec4的构造器的参数 + vertexColor = vec4(0.5f, 0.0f, 0.0f, 1.0f); // 把输出变量设置为暗红色 +} +``` + +片段着色器: + +``` +#version 330 core +in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同) + +out vec4 color; // 片段着色器输出的变量名可以任意命名,类型必须是vec4 + +void main() +{ + color = vertexColor; +} +``` + + + +### GLSL的特点 + +OpenGLES的着色器语言GLSL是一种高级的图形化编程语言,其源自应用广泛的C语言。与传统的C语言不同的是,它提供了更加丰富的针对于图像处理的原生类型,诸如向量、矩阵之类。GLSL主要包含以下特性: + +- GLSL是一种面向过程的语言,和c相同 +- GLSL的基本语法与C/C++相同 +- 它完美的支持向量和矩阵的操作 +- 它是通过限定符操作来管理输入输出类型的 +- GLSL提供了大量的内置函数来提供丰富的扩展功能 + + + +### 基本数据类型 + +GLSL中的数据类型主要分为标量、向量、矩阵、采样器、结构体、数组、空类型七种类型。如下: + +- 标量 + + 标量表示的是只有大小没有方向的量,在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中矩阵拥有2*2、3*3、4*4三种类型的矩阵,分别用mat2、mat3、mat4表示。我们可以把矩阵看做是一个二维数组,也可以用二维数组下表的方式取里面具体位置的值。 + +- 采样器 + + 采样器是专门用来对纹理进行采样工作的,在GLSL中一般来说,一个采样器变量表示一副或者一套纹理贴图。所谓的纹理贴图可以理解为我们看到的物体上的皮肤。 + +- 结构体 + + 和C语言中的结构体相同,用struct来定义结构体。 + +- 数组 + + 数组也与C相同 + +- 空类型 + + void + +### 变量修饰符 + +- none:(默认的可省略)本地变量,可读可写,函数的输入参数既是这种类型 +- const:声明变量或函数的参数为只读类型 +- attribute:用于保存顶点或法线数据,它可以在数据缓冲区中读取数据,仅能用于顶点着色器 +- uniform:在运行时 shader 无法改变 uniform 变量,一般用来放置程序传递给 shader 的变换矩阵,材质,光照参数等等,可用于顶点着色器和片元着色器 +- varying:易变量,用于修饰从顶点着色器向片元着色器传递的变量。一般是在光栅化图元的过程中计算生成,记录在每个片段中(而不是从顶点着色器直接传递给片元着色器) + + + +### 运算符 + +[]、++、-、+、?、:、> 等等 + + + +### 类型转换 + +GLSL的类型转换与C不同。在GLSL中类型不可以自动提升,比如float a=1;就是一种错误的写法,必须严格的写成float a=1.0,也不可以强制转换,即float a=(float)1;也是错误的写法,但是可以用内置函数来进行转换,如float a=float(1);还有float a=float(true);(true为1.0,false为0.0)等,值得注意的是,低精度的int不能转换为低精度的float + +### 限定符 + +与Java中的限定符类似,放在变量类型前面,并且只能用于全局变量。 + +- attritude:一般用于各个顶点各不相同的量。如顶点颜色、坐标等。 +- uniform:一般用于对于3D物体中所有顶点都相同的量。比如光源位置,统一变换矩阵等。 +- varying:表示易变量,一般用于顶点着色器传递到片元着色器的量。 +- const:常量。 + +- 流程控制 + + 常用的与java基本一样 + +- 函数 + + 定义函数的方式也与C语言基本相同。函数的返回值可以是GLSL中的除了采样器的任意类型。对于GLSL中函数的参数,可以用参数用途修饰符来进行修饰,常用修饰符如下: + + - in:输入参数,无修饰符时默认为此修饰符。 + - out:输出参数。 + - inout:既可以作为输入参数,又可以作为输出参数。 + +- 浮点精度 + + 与顶点着色器不同的是,在片元着色器中使用浮点型时,必须指定浮点类型的精度,否则编译会报错。精度有三种,分别为: + + - lowp:低精度。8位。 + - mediump:中精度。10位。 + - highp:高精度。16位。 + +- 程序结构 + + 也是main()为入口函数、全局变量、局部变量等,与java类似。 + +### GLSL内建变量 + +在着色器中有一些特殊的变量,不用声明也可以使用,这些变量叫做内建变量。 他们大致可以分为两种,一种是input类型,负责向硬件(渲染管线)发送数据;另一种是output类型,负责向程序回传数据,以便编程时需要。内建变量相当于着色器硬件的输入和输出点,使用者利用这些输入点输入之后,就会看到屏幕上的输出。通过输出点可以知道输出的某些数据内容。 + + #### 顶点着色器的内建变量 + +- 输入变量 + - gl_Position:顶点坐标信息 + - gl_PointSize:点的大小,默认是1,只有在gl.POINTS模式下才有效 + +片段着色器的内建变量 + +- 输入变量 + - gl_FragCoord:当前片元在framebuffer画面的相对位置 + - gl_FragFacing:bool型,表示是否为属于光栅化生成此片元的对应图元的正面。 + - gl_PointCoord:经过插值计算后的纹理坐标,点的范围是0.0到1.0 +- 输出变量 + - gl_FragColor:当前片元颜色 + - gl_FragData:vec4类型的数据。设置当前片元的颜色,供渲染管线的后继过程使用。 + +### 内置函数 + +#### 常用函数 + +- radians(x):角度转弧度 +- degrees(x):弧度转角度 +- sin(x):正弦函数,传入值为弧度。相同的还有cos余弦函数、tan正切函数、asin反正弦、acos反余弦 +- atan反正切 +- pow(x,y):xy +- exp(x):ex +- exp2(x):2x +- log(x):logex +- log2(x):log2x +- sqrt(x):x√ +- inversesqr(x):1x√ +- abs(x):取x的绝对值 +- sign(x):x>0返回1.0,x<0返回-1.0,否则返回0.0 +- ceil(x):返回大于或者等于x的整数 +- floor(x):返回小于或者等于x的整数 +- fract(x):返回x-floor(x)的值 +- mod(x,y):取模(求余) +- min(x,y):获取xy中小的那个 +- max(x,y):获取xy中大的那个 +- mix(x,y,a):返回x∗(1−a)+y∗a +- step(x,a):x< a返回0.0,否则返回1.0 +- smoothstep(x,y,a):a < x返回0.0,a>y返回1.0,否则返回0.0-1.0之间平滑的Hermite插值。 +- dFdx(p):p在x方向上的偏导数 +- dFdy(p):p在y方向上的偏导数 +- fwidth(p):p在x和y方向上的偏导数的绝对值之和 + + + +#### 几何函数 + +- length(x):计算向量x的长度 +- distance(x,y):返回向量xy之间的距离 +- dot(x,y):返回向量xy的点积 +- cross(x,y):返回向量xy的差积 +- normalize(x):返回与x向量方向相同,长度为1的向量 + +#### 矩阵函数 + +- matrixCompMult(x,y):将矩阵相乘 +- lessThan(x,y):返回向量xy的各个分量执行x< y的结果,类似的有greaterThan,equal,notEqual +- lessThanEqual(x,y):返回向量xy的各个分量执行x<= y的结果,类似的有类似的有greaterThanEqual +- any(bvec x):x有一个元素为true,则为true +- all(bvec x):x所有元素为true,则返回true,否则返回false +- not(bvec x):x所有分量执行逻辑非运算 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/VideoDevelopment/OpenGL/7.GLES\347\261\273\345\217\212Matrix\347\261\273.md" "b/VideoDevelopment/OpenGL/7.GLES\347\261\273\345\217\212Matrix\347\261\273.md" new file mode 100644 index 00000000..72141be7 --- /dev/null +++ "b/VideoDevelopment/OpenGL/7.GLES\347\261\273\345\217\212Matrix\347\261\273.md" @@ -0,0 +1,143 @@ +## 7.GLES类及Matrix类 + + + +GLES30作为我们与着色器连接的工具类提供了丰富的api。 + +上一篇文章说到GLSL中的变量修饰符有以下部分: + +- none:(默认的可省略)本地变量,可读可写,函数的输入参数既是这种类型 +- const:声明变量或函数的参数为只读类型 +- attribute:用于保存顶点或法线数据,它可以在数据缓冲区中读取数据,仅能用于顶点着色器 +- uniform:在运行时 shader 无法改变 uniform 变量,一般用来放置程序传递给 shader 的变换矩阵,材质,光照参数等等,可用于顶点着色器和片元着色器 +- varying:用于修饰从顶点着色器向片元着色器传递的变量 + + + +### 获取着色器程序内成员变量的id(句柄、指针) + +- mPostionHandler = GLES30.glGetAttribLocation(mProgram, "aPosition"):获取着色器程序中,指定为attribute类型的变量id。 +- mMatrixHnadler = GLES30.glGetUniformLocation(mProgram, "uMVPMatrix"):获取着色器程序中,指定为uniform类型的变量id。 + +### 向着色器传递数据 + +上面获取到指向着色器中相应数据成员的各个id后,就能将我们要设置的顶点数据、颜色数据等传递到着色器中了。 + +``` +// 将最终变换矩阵传入shader程序 +GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, MatrixState.getFinalMatrix(), 0); +// 顶点位置数据传入着色器 +GLES20.glVertexAttribPointer(maPositionHandle, 3, GLES20.GL_FLOAT, false, 20, mRectBuffer); +// 顶点颜色数据传入着色器中 +GLES20.glVertexAttribPointer(maColorHandle, 4, GLES20.GL_FLOAT, false, 4*4, mColorBuffer); +// 顶点坐标传递到顶点着色器 +GLES20.glVertexAttribPointer(maTextureHandle, 2, GLES20.GL_FLOAT, false, 20, mRectBuffer); +``` + +### 定义顶点属性数组 + + + + /** + * 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); + + +### 启用或禁用顶点属性数组 + +调用GLES20.glEnableVertexAttribArray和GLES20.glDisableVertexAttribArray传入参数index。如果启用,那么当GLES20.glDrawArrays或者GLES20.glDrawElements被调用时,顶点属性数组会被使用。 + +### 选择活动纹理单元 + +``` +void glActiveTexture(int texture) +``` + +texture指定哪一个纹理单元被置为活动状态。texture必须是GL_TEXTUREi之一,其中0 <= i < GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS,初始值为GL_TEXTURE0。 +GLES20.glActiveTexture()确定了后续的纹理状态改变影响哪个纹理,纹理单元的数量是依据该纹理单元所被支持的具体实现。 + +## Matrix + +而`Matrix就是专门设计出来帮助我们简化矩阵和向量运算操作的,里面所有的实现原理都是线性代数中的运算`。 + +我们知道OpenGl中实现图形的操作大量使用了矩阵,在OpenGL中使用的向量为列向量,我们通过利用矩阵与列向量(颜色、坐标都可看做列向量)相乘,得到一个新的列向量。利用这点,我们构建一个的矩阵,与图形所有的顶点坐标坐标相乘,得到新的顶点坐标集合,当这个矩阵构造恰当的话,新得到的顶点坐标集合形成的图形相对原图形就会出现平移、旋转、缩放或拉伸、抑或扭曲的效果。 +`Matrix:专门为处理4*4矩阵和4元素向量设计的,其中的方法都是static的,不需要初始化Matrix实例`。 + +- multiplyMM + + 两个4x4矩阵相乘,并将结果存储到第三个4x4矩阵中。 + + ``` + public static native void multiplyMM(float[] result, int resultOffset, + float[] lhs, int lhsOffset, float[] rhs, int rhsOffset); + ``` + +- multiplyMV + + 将一个4x4矩阵和一个四元素向量相乘,得到一个新的四元素向量 + + ``` + public static native void multiplyMV(float[] resultVec,int resultVecOffset, + float[] lhsMat, int lhsMatOffset, + float[] rhsVec, int rhsVecOffset); + ``` + +- transposeM + + 获取逆矩阵 + +- invertM + + 计算正交投影和透视投影 + +- orthoM + + 计算正交投影矩阵 + +- frustumM + + 计算透视投影矩阵 + +- perspectiveM + + 根据视场角度、纵横比和Z裁剪平面定义投影矩阵 + +- length + + 计算向量长度 + +- setldentityM + + 创建单位矩阵 + +- scaleM + +- translateM + +- rotateM + +- setRotateM + +- setRotateEulerM + +- setLookAtm + + 定义相机视图 + + + + + + + + + + + diff --git "a/VideoDevelopment/OpenGL/8.OpenGL ES\347\272\271\347\220\206.md" "b/VideoDevelopment/OpenGL/8.OpenGL ES\347\272\271\347\220\206.md" new file mode 100644 index 00000000..f7c28838 --- /dev/null +++ "b/VideoDevelopment/OpenGL/8.OpenGL ES\347\272\271\347\220\206.md" @@ -0,0 +1,547 @@ +## OpenGL ES纹理 + + + +### 纹理 + +在OpenGL中简单理解就是一张图片。 + +- 纹理Id:纹理的直接饮用 + +- 纹理单元:纹理的操作容器,有GL_TEXTURE0、GL_TEXTURE1、GL_TEXTURE2等,纹理单元的数量是有限的,最多16个。所以在最多只能同时操作16个纹理。在切换使用纹理单元的时候,使用glActiveTexture方法。 + +- 纹理目标:一个纹理单元中包含了多个类型的纹理目标,有GL_TEXTURE_1D、GL_TEXTURE_2D、CUBE_MAP等。本章中,将纹理ID绑定到纹理单元0的GL_TEXTURE_2D纹理目标上,之后对纹理目标的操作都是对纹理Id对应的数据进行操作。 + + OpenGL要操作一个纹理,那么是将纹理ID装进纹理单元这个容器里,然后再通过操作纹理单元的方式去实现的。这样的话,我们可以加载出很多很多个纹理ID(但要注意爆内存问题),但只有16个纹理单元,在Fragment Shader里最多同时能操作16个单元。 + + + +### 纹理与渐变色的区别 + +渐变色:光栅化过程中计算出颜色值,然后再片段着色器的时候可以直接赋值。 + +纹理:光栅化过程中,计算出当前片段在纹理上的坐标位置,然后在片段着色器中根据这个纹理上的坐标,去纹理中取出相应的颜色值。 + +### 纹理坐标 + +OpenGL中,2D纹理也有自己的坐标体系,取值范围在(0,0)到(1,1)内,两个维度分别为S、T,所以一般称为ST纹理坐标。有些时候也叫UV坐标。纹理左边的方向性和Android上的canvas移植,都是顶点在左上角。 + +纹理上的每个顶点与定点坐标上的顶点一一对应。如下图,左边是顶点坐标,右边是纹理坐标,只要两个坐标的ABCD定义顺序一致,就可以正常地映射出对应的图形。顶点坐标内光栅化后的每个片段,都会在纹理坐标内取得对应的颜色值。 + + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_es_texture_position.jpg) + +### 纹理尺寸 + +在OpenGL ES 2.0中规定,纹理的每个维度必须是2次幂,也就是2、4、8....等等,纹理的最大值上限通常比较大,例如2048*2048。 + +### 文件读取 + +OpenGL不能直接加载jpg或者png这类被编码的压缩格式,需要加载原始数据,也就是bitmpa。我们在内置图片到工程中,应该将图片放到drawable-nodpi中,避免读取时被压缩,通过BitmapFactory解码读取图片时,要设置为非缩放的方式,即options.isScaled=false。 + +### 纹理过滤 + +当我们通过光栅化将图形处理成一个个小片段的时候,再讲纹理采样,渲染到指定位置上时,通常会遇到纹理元素和小片段并非一一对应。这时候,会出现纹理的压缩或者放大。那么在这两种情况下,就会有不同的处理方案,这就是纹理过滤了。 + +### 加载纹理 + +下面是一个工具类方法,相对通用,能解决大部分需求,这个方法可以将内置的图片资源加载出对应的纹理ID。 + + ```java +/** + * 纹理加载助手类 + */ +public class TextureHelper { + private static final String TAG = "TextureHelper"; + + /** + * 根据资源ID获取相应的OpenGL纹理ID,若加载失败则返回0 + *
必须在GL线程中调用 + */ + public static TextureBean loadTexture(Context context, int resourceId) { + TextureBean bean = new TextureBean(); + final int[] textureObjectIds = new int[1]; + // 1. 创建纹理对象 + GLES20.glGenTextures(1, textureObjectIds, 0); + + if (textureObjectIds[0] == 0) { + if (LoggerConfig.ON) { + Log.w(TAG, "Could not generate a new OpenGL texture object."); + } + return bean; + } + + final BitmapFactory.Options options = new BitmapFactory.Options(); + options.inScaled = false; + + final Bitmap bitmap = BitmapFactory.decodeResource( + context.getResources(), resourceId, options); + + if (bitmap == null) { + if (LoggerConfig.ON) { + Log.w(TAG, "Resource ID " + resourceId + " could not be decoded."); + } + // 加载Bitmap资源失败,删除纹理Id + GLES20.glDeleteTextures(1, textureObjectIds, 0); + return bean; + } + // 2. 将纹理绑定到OpenGL对象上 + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureObjectIds[0]); + + // 3. 设置纹理过滤参数:解决纹理缩放过程中的锯齿问题。若不设置,则会导致纹理为黑色 + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR_MIPMAP_LINEAR); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + // 4. 通过OpenGL对象读取Bitmap数据,并且绑定到纹理对象上,之后就可以回收Bitmap对象 + GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0); + + // Note: Following code may cause an error to be reported in the + // ADB log as follows: E/IMGSRV(20095): :0: HardwareMipGen: + // Failed to generate texture mipmap levels (error=3) + // No OpenGL error will be encountered (glGetError() will return + // 0). If this happens, just squash the source image to be + // square. It will look the same because of texture coordinates, + // and mipmap generation will work. + // 5. 生成Mip位图 + GLES20.glGenerateMipmap(GLES20.GL_TEXTURE_2D); + + // 6. 回收Bitmap对象 + bean.setWidth(bitmap.getWidth()); + bean.setHeight(bitmap.getHeight()); + bitmap.recycle(); + + // 7. 将纹理从OpenGL对象上解绑 + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); + + // 所以整个流程中,OpenGL对象类似一个容器或者中间者的方式,将Bitmap数据转移到OpenGL纹理上 + bean.setTextureId(textureObjectIds[0]); + return bean; + } + + /** + * 纹理数据 + */ + public static class TextureBean { + private int mTextureId; + private int mWidth; + private int mHeight; + + public int getTextureId() { + return mTextureId; + } + + void setTextureId(int textureId) { + mTextureId = textureId; + } + + public int getWidth() { + return mWidth; + } + + public void setWidth(int width) { + mWidth = width; + } + + public int getHeight() { + return mHeight; + } + + public void setHeight(int height) { + mHeight = height; + } + } +} + ``` + +上面的工具类要求图片必须是2次幂的尺寸,不过大部分情况下应该不会有问题。 + + + +### 纹理实现 + +- 顶点着色器 + + 将之前颜色向量`vec4 aColor`变为了纹理向量`vec2 aTextureCoord`; + + ``` + #version 300 es + layout (location = 0) in vec4 vPosition; + layout (location = 1) in vec2 aTextureCoord; + uniform mat4 u_Matrix; + //输出纹理坐标(s,t) + out vec2 vTexCoord; + void main() { + gl_Position = u_Matrix*vPosition; + gl_PointSize = 10.0; + vTexCoord = aTextureCoord; + } + ``` + + + +- 片段着色器 + + 之前直接输出顶点着色器来的颜色,现在变为经过纹理处理最终成为输出颜色`。 + + ``` + #version 300 es + precision mediump float; + uniform sampler2D uTextureUnit; + //接收刚才顶点着色器传入的纹理坐标(s,t) + in vec2 vTexCoord; + out vec4 vFragColor; + void main() { + vFragColor = texture(uTextureUnit,vTexCoord); + } + ``` + + + +- render实现类 + + 区别就是把前面绘制矩形时的color部分换成了纹理,其他都是一样的。 + + 纹理的绘制: + + ``` + //激活纹理,设置当前活动的纹理单元为单元0 + GLES30.glActiveTexture(GLES30.GL_TEXTURE0); + //绑定纹理,将纹理id绑定到当前活动的纹理单元上 + GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureId); + // 将纹理单元传递片段着色器的u_TextureUnit + GLES20.glUniform1i(aTextureLocation, 0); + ``` + + + + + + ```java + public class TextureRender implements GLSurfaceView.Renderer { + //一个Float占用4Byte + private static final int BYTES_PER_FLOAT = 4; + //顶点位置缓存 + private final FloatBuffer vertexBuffer; + //顶点颜色缓存 + private final FloatBuffer textureBuffer; + //渲染程序 + private int mProgram; + + //返回属性变量的位置 + //变换矩阵 + private int uMatrixLocation; + //位置 + private int aPositionLocation; + //颜色 + private int aTextureLocation; + private int textureId; + + /** + * 坐标占用的向量个数 + */ + private static final int POSITION_COMPONENT_COUNT = 2; + private static final float[] POINT_DATA = { + -0.5f, -0.5f, + 0.5f, -0.5f, + -0.5f, 0.5f, + 0.5f, 0.5f, + }; + /** + * 颜色占用的向量个数 + */ + private static final int TEXTURE_COMPONENT_COUNT = 2; + private static final float[] TEXTURE_DATA = { + 0.0f, 1.0f, + 1.0f, 1.0f, + 0.0f, 0.0f, + 1.0f, 0.0f + }; + private final float[] mProjectionMatrix = new float[16]; + + public TextureRender() { + //顶点位置相关 + //分配本地内存空间,每个浮点型占4字节空间;将坐标数据转换为FloatBuffer,用以传入给OpenGL ES程序 + vertexBuffer = ByteBuffer.allocateDirect(POINT_DATA.length * BYTES_PER_FLOAT) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + vertexBuffer.put(POINT_DATA); + vertexBuffer.position(0); + + //顶点颜色相关 + textureBuffer = ByteBuffer.allocateDirect(TEXTURE_DATA.length * BYTES_PER_FLOAT) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + textureBuffer.put(TEXTURE_DATA); + textureBuffer.position(0); + } + + @Override + public void onSurfaceCreated(GL10 gl, EGLConfig config) { + //将背景设置为白色 + GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f); + + //编译顶点着色程序 + String vertexShaderStr = ResReadUtils.readResource(R.raw.texture_vertex_simple_shade); + int vertexShaderId = ShaderUtils.compileVertexShader(vertexShaderStr); + //编译片段着色程序 + String fragmentShaderStr = ResReadUtils.readResource(R.raw.texture_fragment_simple_shade); + int fragmentShaderId = ShaderUtils.compileFragmentShader(fragmentShaderStr); + //连接程序 + mProgram = ShaderUtils.linkProgram(vertexShaderId, fragmentShaderId); + //在OpenGLES环境中使用程序 + GLES30.glUseProgram(mProgram); + + + uMatrixLocation = GLES30.glGetUniformLocation(mProgram, "u_Matrix"); + aPositionLocation = GLES30.glGetAttribLocation(mProgram, "vPosition"); + aTextureLocation = GLES30.glGetAttribLocation(mProgram, "aTextureCoord"); + textureId = TextureHelper.loadTexture(MyApplication.getInstance(), R.drawable.img).getTextureId(); + } + + @Override + public void onSurfaceChanged(GL10 gl, int width, int height) { + //设置绘制窗口 + GLES30.glViewport(0, 0, width, height); + //正交投影方式 + final float aspectRatio = width > height ? + (float) width / (float) height : + (float) height / (float) width; + if (width > height) { + //横屏 + Matrix.orthoM(mProjectionMatrix, 0, -aspectRatio, aspectRatio, -1f, 1f, -1f, 1f); + } else { + //竖屏 + Matrix.orthoM(mProjectionMatrix, 0, -1f, 1f, -aspectRatio, aspectRatio, -1f, 1f); + } + } + + @Override + public void onDrawFrame(GL10 gl) { + //把颜色缓冲区设置为我们预设的颜色 + GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT); + + //将变换矩阵传入顶点渲染器 + GLES20.glUniformMatrix4fv(uMatrixLocation, 1, false, mProjectionMatrix, 0); + //准备坐标数据 + GLES30.glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GLES30.GL_FLOAT, false, 0, vertexBuffer); + //启用顶点位置句柄 + GLES30.glEnableVertexAttribArray(aPositionLocation); + + //准备颜色数据 + GLES30.glVertexAttribPointer(aTextureLocation, TEXTURE_COMPONENT_COUNT, GLES30.GL_FLOAT, false, 0, textureBuffer); + //启用顶点颜色句柄 + GLES30.glEnableVertexAttribArray(aTextureLocation); + + //激活纹理 + GLES30.glActiveTexture(GLES30.GL_TEXTURE0); + //绑定纹理 + GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureId); + // 将纹理单元传递片段着色器的u_TextureUnit + GLES20.glUniform1i(aTextureLocation, 0); + + + // 开始绘制 + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, POINT_DATA.length / POSITION_COMPONENT_COUNT); + //禁止顶点数组的句柄 + GLES30.glDisableVertexAttribArray(aPositionLocation); + GLES30.glDisableVertexAttribArray(aTextureLocation); + } + } + ``` + + + +### 多纹理绘制 + + + +- 单纹理单元,多次绘制 + + 多次调用glDrawArrays绘制纹理顶点的方式来实现,这样就是一张一张的按先后顺序,一层一层的绘制到当前的一帧画面。着色器与上面的完全一致,唯一不同的是要提供两个顶点位置的坐标,然后分别用这两个坐标调用两次glDrawArrays进行绘制 + + ``` + public class TextureRender implements GLSurfaceView.Renderer { + //一个Float占用4Byte + private static final int BYTES_PER_FLOAT = 4; + //顶点位置缓存 + private final FloatBuffer vertexBuffer; + private final FloatBuffer vertexBuffer2; + //顶点颜色缓存 + private final FloatBuffer textureBuffer; + //渲染程序 + private int mProgram; + + //返回属性变量的位置 + //变换矩阵 + private int uMatrixLocation; + //位置 + private int aPositionLocation; + //颜色 + private int aTextureLocation; + private int textureId; + private int textureId2; + + /** + * 坐标占用的向量个数 + */ + 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; + private static final float[] TEXTURE_DATA = { + 0.0f, 1.0f, + 1.0f, 1.0f, + 0.0f, 0.0f, + 1.0f, 0.0f + }; + private final float[] mProjectionMatrix = new float[16]; + + public TextureRender() { + //顶点位置相关 + //分配本地内存空间,每个浮点型占4字节空间;将坐标数据转换为FloatBuffer,用以传入给OpenGL ES程序 + vertexBuffer = ByteBuffer.allocateDirect(POINT_DATA.length * BYTES_PER_FLOAT) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + vertexBuffer.put(POINT_DATA); + vertexBuffer.position(0); + + vertexBuffer2 = ByteBuffer.allocateDirect(POINT_DATA2.length * BYTES_PER_FLOAT) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + vertexBuffer2.put(POINT_DATA2); + vertexBuffer2.position(0); + + //顶点颜色相关 + textureBuffer = ByteBuffer.allocateDirect(TEXTURE_DATA.length * BYTES_PER_FLOAT) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + textureBuffer.put(TEXTURE_DATA); + textureBuffer.position(0); + } + + @Override + public void onSurfaceCreated(GL10 gl, EGLConfig config) { + //将背景设置为白色 + GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f); + + //编译顶点着色程序 + String vertexShaderStr = ResReadUtils.readResource(R.raw.texture_vertex_simple_shade); + int vertexShaderId = ShaderUtils.compileVertexShader(vertexShaderStr); + //编译片段着色程序 + String fragmentShaderStr = ResReadUtils.readResource(R.raw.texture_fragment_simple_shade); + int fragmentShaderId = ShaderUtils.compileFragmentShader(fragmentShaderStr); + //连接程序 + mProgram = ShaderUtils.linkProgram(vertexShaderId, fragmentShaderId); + //在OpenGLES环境中使用程序 + GLES30.glUseProgram(mProgram); + + uMatrixLocation = GLES30.glGetUniformLocation(mProgram, "u_Matrix"); + aPositionLocation = GLES30.glGetAttribLocation(mProgram, "vPosition"); + aTextureLocation = GLES30.glGetAttribLocation(mProgram, "aTextureCoord"); + textureId = TextureHelper.loadTexture(MyApplication.getInstance(), R.drawable.img).getTextureId(); + textureId2 = TextureHelper.loadTexture(MyApplication.getInstance(), R.drawable.img).getTextureId(); + } + + @Override + public void onSurfaceChanged(GL10 gl, int width, int height) { + //设置绘制窗口 + GLES30.glViewport(0, 0, width, height); + //正交投影方式 + final float aspectRatio = width > height ? + (float) width / (float) height : + (float) height / (float) width; + if (width > height) { + //横屏 + Matrix.orthoM(mProjectionMatrix, 0, -aspectRatio, aspectRatio, -1f, 1f, -1f, 1f); + } else { + //竖屏 + Matrix.orthoM(mProjectionMatrix, 0, -1f, 1f, -aspectRatio, aspectRatio, -1f, 1f); + } + } + + @Override + public void onDrawFrame(GL10 gl) { + //把颜色缓冲区设置为我们预设的颜色 + GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT); + + //将变换矩阵传入顶点渲染器 + GLES20.glUniformMatrix4fv(uMatrixLocation, 1, false, mProjectionMatrix, 0); + //准备坐标数据 + GLES30.glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GLES30.GL_FLOAT, + false, 0, vertexBuffer); + //启用顶点位置句柄 + GLES30.glEnableVertexAttribArray(aPositionLocation); + + //准备颜色数据 + GLES30.glVertexAttribPointer(aTextureLocation, TEXTURE_COMPONENT_COUNT, GLES30.GL_FLOAT, + false, 0, textureBuffer); + //启用顶点颜色句柄 + GLES30.glEnableVertexAttribArray(aTextureLocation); + + //激活纹理 + GLES30.glActiveTexture(GLES30.GL_TEXTURE0); + //绑定纹理 + GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureId); + // 将纹理单元传递片段着色器的u_TextureUnit + GLES20.glUniform1i(aTextureLocation, 0); + // 开始绘制 + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, POINT_DATA.length / POSITION_COMPONENT_COUNT); + + + //准备第二个纹理的坐标数据 + GLES30.glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GLES30.GL_FLOAT, + false, 0, vertexBuffer2); + //启用顶点位置句柄 + GLES30.glEnableVertexAttribArray(aPositionLocation); + //绑定纹理,前面已经激活了,就不用再调了 + GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureId2); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, POINT_DATA.length / POSITION_COMPONENT_COUNT); + + //禁止顶点数组的句柄 + GLES30.glDisableVertexAttribArray(aPositionLocation); + GLES30.glDisableVertexAttribArray(aTextureLocation); + } + } + ``` + + + +- 多纹理单元,单词绘制 + + OpenGL可以同时操作的纹理单元是16个,那么我们可以利用多个纹理单元来进行绘制同一个图层,从而达到目的。 + + + + + + + + + + + + + + + + + + + + + diff --git "a/VideoDevelopment/OpenGL/9.GLSurfaceView+MediaPlayer\346\222\255\346\224\276\350\247\206\351\242\221.md" "b/VideoDevelopment/OpenGL/9.GLSurfaceView+MediaPlayer\346\222\255\346\224\276\350\247\206\351\242\221.md" new file mode 100644 index 00000000..b6682af6 --- /dev/null +++ "b/VideoDevelopment/OpenGL/9.GLSurfaceView+MediaPlayer\346\222\255\346\224\276\350\247\206\351\242\221.md" @@ -0,0 +1,108 @@ +## 9.GLSurfaceView+MediaPlayer播放视频 + + + +平时的视频播放都是使用mediaplayer+textureview或者mediaplayer+surfaceview,但是如果我们要对视频进行一些OpenGL的操作,打个比方说我要在视频播放的时候添加一个滤镜,这个时候就需要用都SurfaceTexture了。 + + + +### TextureView+MediaPlayer播放视频 + +```java +public class VideoActivity extends Activity { + private static final String VIDEO_PATH = "http://60.28.125.129/video19.ifeng.com/video06/2012/04/11/629da9ec-60d4-4814-a940-997e6487804a.mp4"; + private Surface mSurface; + private TextureView mTextureView; + private MediaPlayer mMediaPlayer; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_video); + mTextureView = findViewById(R.id.textureview); + mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { + // 创建surface对象,让其从surfacetexture中获取数据 + mSurface = new Surface(surface); + startPlay(); + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { + + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { + mSurface = null; + if (mMediaPlayer != null) { + mMediaPlayer.stop(); + mMediaPlayer.release(); + mMediaPlayer = null; + } + return true; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surface) { + + } + }); + } + + @Override + protected void onResume() { + super.onResume(); + if (mTextureView.isAvailable()) { + startPlay(); + } + } + + private void startPlay() { + if (mMediaPlayer != null) { + return; + } + mMediaPlayer = new MediaPlayer(); + try { + mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + mMediaPlayer.setDataSource(this, Uri.parse(VIDEO_PATH)); + mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { + @Override + public void onPrepared(MediaPlayer mp) { + if (mp != null) { + mp.start(); + } + } + }); + mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() { + @Override + public boolean onError(MediaPlayer mp, int what, int extra) { + return false; + } + }); + // 将surface设置给mediaplayer + mMediaPlayer.setSurface(mSurface); + mMediaPlayer.setScreenOnWhilePlaying(true); + mMediaPlayer.setLooping(true); + mMediaPlayer.prepareAsync(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + protected void onPause() { + super.onPause(); + if (mMediaPlayer != null) { + mMediaPlayer.reset(); + mMediaPlayer.release(); + mMediaPlayer = null; + } + } +} +``` + + + +### 增加OpenGL ES \ No newline at end of file diff --git "a/VideoDevelopment/OpenGL\347\256\200\344\273\213.md" "b/VideoDevelopment/OpenGL\347\256\200\344\273\213.md" deleted file mode 100644 index faa9d102..00000000 --- "a/VideoDevelopment/OpenGL\347\256\200\344\273\213.md" +++ /dev/null @@ -1,52 +0,0 @@ -### OpenGL简介 - -OpenGL(Open Graphics Library开发图形接口)是一个跨平台的图形API,用于指定3D图形处理硬件中的标准软件接口。 - -OpenGl的前身是SGI公司为其图形工作站开发的IRIS GL,后来因为IRIS GL的移植性不好,所以在其基础上,开发出了OpenGl。OpenGl一般用于在图形工作站,PC端使用,由于性能各方面原因,在移动端使用OpenGl基本带不动。为此,Khronos公司就为OpenGl提供了一个子集,OpenGl ES(OpenGl for Embedded System)。 - - - -### OpenGL ES - -OpenGl ES是免费的跨平台的功能完善的2D/3D图形库接口的API,是OpenGL的一个子集。 - -移动端使用到的基本上都是OpenGl ES,当然Android开发下还专门为OpenGl提供了android.opengl包,并且提供了GlSurfaceView,GLU,GlUtils等工具类。 - - - -### OpenGL的作用 - - - -手机上做图像处理用很多方法,但是目前为止最高效的方法就是有效的使用图形处理单元(GPU),图像的处理和渲染就是在将要渲染到窗口上的像素上做很多的浮点匀速,而GPU可以并行的做浮点运算,所以用GPU来分担CPU的部分,可以提高效率。 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git "a/VideoDevelopment/SurfaceView\344\270\216TextureView.md" "b/VideoDevelopment/SurfaceView\344\270\216TextureView.md" index 9af6ad15..4234918a 100644 --- "a/VideoDevelopment/SurfaceView\344\270\216TextureView.md" +++ "b/VideoDevelopment/SurfaceView\344\270\216TextureView.md" @@ -8,7 +8,7 @@ SurfaceView与TextureView ### `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`语言中,可以通过一个文件的句柄,就可以获得文件的内容一样; 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 21cb5ad1..ac0d3d9b 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" @@ -14,9 +14,48 @@ MP4是一种格式的规范,这个规范是被ISO机构认证的,你如果 - P帧(Predictive coded picture):预测编码图像帧简称差别帧,这一帧跟之前的一个关键帧或P帧的差别,解码时需要用之前缓存的画面叠加上本帧定义的差别,生成最终的画面。也就是说P帧没有完整的画面数据,只有与前一帧的画面差别的数据。 - B帧(Bidirectionally predicted picture):双向预测编码图像帧简称双向差别帧,也就是B帧记录的是本帧与前后帧的差别。也就是要解码B帧,不仅要取得之前的缓存画面,还要解码之后的画面,通过前后画面与本帧数据的叠加来获取最终的画面。B帧压缩率高,但是解码时会更耗CPU。 - - GOP(Group of Pictures)是一组连续的画面,由一张I帧和数张B/P帧组成,是视频图像编码器和解码器存取的基本单位,它的排列顺序将会一直重复到影像结束。 编码器将多张图像进行编码后生产成一段一段的 GOP ,解码器在播放时则是读取一段一段的GOP进行解码后读取画面再渲染显示。 + + + +### 数据压缩比 + +数据压缩比大约为: + +I帧 : P帧 : B帧 = 7 :20 : 50 + +P帧和B帧极大的节省了数据量,节省出来的空间可以用来多保存一些I帧,以实现在相同码率下,提供更好的画质。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/VideoDevelopment/\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" "b/VideoDevelopment/\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" index 45b3f6bf..398deaa6 100644 --- "a/VideoDevelopment/\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" +++ "b/VideoDevelopment/\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" @@ -6,18 +6,39 @@ --- - ***媒体***:是表示,传输,存储信息的载体,常人们见到的文字、声音、图像、图形等都是表示信息的媒体。 + - ***多媒体***:是声音、动画、文字、图像和录像等各种媒体的组合,以图文并茂,生动活泼的动态形式表现出来,给人以很强的视觉冲击力,留下深刻印象. + - ***多媒体技术***:是将文字、声音、图形、静态图像、动态图像与计算集成在一起的技术。它要解决的问题是计算机进一步帮助人类按最自然的和最习惯的方式接受和处理信息。 + - ***流媒体***:流媒体是指采用流式传输的方式在`Internet`播放的连续时基媒体格式,实际指的是一种新的媒体传送方式,而不是一种新的媒体格式(在网络上传输音/视频等多媒体信息现在主要有下载和流式传输两种方式)流式传输分两种方法:实时流式传输方式(`Realtime Streaming`)和顺序流式传输方式(`Progressive Streaming`)。 + - ***多媒体文件***:是既包括视频又包括音频,甚至还带有脚本的一个集合,也可以叫容器。 + - ***媒体编码***:是文件当中的视频和音频所采用的压缩算法。也就是说一个`avi`的文件,当中的视频编码有可能是`A`,也可能是`B`,而其音频编码有可能是`1`,也有可能是`2`。 + - ***转码***:指将一段多媒体包括音频、视频或者其他的内容从一种编码格式转换成为另外一种编码格式。 + - ***帧***:帧就是一段数据的组合,它是数据传输的基本单位。就是影像动画中最小单位的单幅影像画面,相当于电影胶片上的每一格镜头。一帧就是一副静止的画面,连续的帧就形成动画,如电视图像等。 + - ***视频***:连续的图象变化每秒超过24帧(`Frame`)画面以上时,根据视觉暂留原理,人眼无法辨别单幅的静态画面,看上去是平滑连续的视觉效果,这样连续的画面叫做视频. + - ***音频***:人类能听到的声音都成为音频,但是一般我们所说到的音频时存储在计算机里的声音。 -- ***码率***:码率就是数据传输时单位时间传送的数据位数,一般我们用的单位是`kbps`即千位每秒。 通俗一点的理解就是取样率,单位时间内取样率越大,精度就越高,处理出来的文件就越接近原始文件,但是文件体积与取样率是成正比的,所以几乎所有的编码格式重视的都是如何用最低的码率达到最少的失真。但是因为编码算法不一样,所以也不能用码率来统一衡量音质或者画质.文件大小(b) = 码率(b/s) * 时长(s)。 + +- ***比特率(码率)***:码率就是数据传输时单位时间传送的数据位数,一般我们用的单位是`kbps`即千位(bit)每秒,也就是每秒钟传送多少个千位的信息。 通俗一点的理解就是取样率,单位时间内取样率越大,精度就越高,处理出来的文件就越接近原始文件,但是文件体积与取样率是成正比的,所以几乎所有的编码格式重视的都是如何用最低的码率达到最少的失真。但是因为编码算法不一样,所以也不能用码率来统一衡量音质或者画质.小写的b表示bit(位),大写的B表示byte(字节),一个字节=8个位,即1B=8b;前面的k表示1024的意思,即1024个位(Kb)或者1024个字节(KB),表示文件的大小单位,一般使用KB。1KB/s=8Kbps。码率(kbps)=文件大小(KB)*8/时间(秒)。 + - 动态码率(VBR: Variable Bit Rate) + + 比特率可以随着图像复杂程度的不同而随之变化。图像内容简单的片段采用较小的码率,图像 + + 内容复杂的片段采用较大的码率,这样既保证了播放质量,又兼顾了数据量的限制。例如RMVB视频文件,其中的VB就是指VBR,表示采用动态比特率编码方式,达到播放质量与体积兼得的效果。 + + - 静态比特率(CBR: Constant Bit Rate) + + 比特率恒定,图像内容复杂的片段质量不稳定,图像内容简单的片段质量较好。 + - ***帧率***:帧/秒(`frames per second`)的缩写帧率即每秒显示帧数,帧率表示图形处理器处理场时每秒钟能够更新的次数。高的帧率可以得到更流畅、更逼真的动画。一般来说`30fps`就是可以接受的,但是将性能提升至`60fps`则可以明显提升交互感和逼真感,但是一般来说超过`75fps`一般就不容易察觉到有明显的流畅度提升了。如果帧率超过屏幕刷新率只会浪费图形处理的能力,因为监视器不能以这么快的速度更新,这样超过新率的帧率就浪费掉了。 -![image](https://github.com/CharonChui/Pictures/blob/master/fps.gif?raw=true) + ![image](https://github.com/CharonChui/Pictures/blob/master/fps.gif?raw=true) + - ***关键帧***:相当于二维动画中的原画,指角色或者物体运动或变化中的关键动作所处的那一帧,它包含了图像的所有信息,后来帧仅包含了改变了的信息。如果你没有足够的关键帧,你的影片品质可能比较差,因为所有的帧从别的帧处产生。对于一般的用途,一个比较好的原则是每5秒设一个关键键。但如果时那种实时传输的流文件,那么要考虑传输网络的可靠度,所以要1到2秒增加一个关键帧。 @@ -162,6 +183,59 @@ PAR DAR SAR - DAR(Display Aspect Ratio):显示横纵比。即最终播放出来的画面的宽和高的比,正规的播放器需要按照DAR来播放视频。 - SAR(Sample Aspect Ratio):采样横纵比。表示横向的像素点数和纵向的像素点数的比值。这也就是我们说的分辨率。 - DAR = PAR x SAR + + + +### 视频播放原理 + +播放一个本地视频文件,需要经过解封装,解码音视频,音视频同步等步骤。 + +![image](https://github.com/CharonChui/Pictures/blob/master/video_player_decode.png?raw=true) + + + +- 解封装: 就是将输入的封装格式的数据,分离成音频压缩编码数据和视频压缩编码数据。例如,FLV格式的数据,经过解封装操作后,输出H264编码的视频码流和AAC编码的音频码流。 + +- 解码: 将视频/音频压缩编码数据,解码成本非压缩的视频/音频原始数据。通过解码,压缩编码的视频数据输出成为非压缩的颜色数据,例如YUV420p,RGB等等;压缩编码的音频数据输出成为非压缩的音频抽样数据,例如PCM数据。 +- 音视频同步: 根据解封装模块处理过程中获取到的参数信息,同步解码出来的视频和音频数据,并将音视频数据送至系统的显卡和声卡播放出来。 + + + +### 流媒体 + +上面播放器的是本地视频文件,如果播放的是网络上的视频,步骤则为: 解协议、解封装、解码音视频、音视频同步,多了一个接协议的步骤。 + +- 解协议: 将流媒体协议的数据,解析为标准的相应的封装格式数据。 + + 音视频在网络上传播的时候,常常采用各种流媒体协议,例如HTTP、RTMP等等,这些协议在传输音视频数据的同时也会传输一些信令数据。这些信令数据包括对播放的控制(播放、暂停、停止)或者对网络状态的描述等。解协议的过程中会去除掉信令数据而只保留音视频数据。例如,采用RTMP协议传输的数据,经过解协议操作后,输出FLV格式的数据。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 79488e4ee00ac5819bb42942f11af851e6918cad Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 18 Mar 2020 16:26:33 +0800 Subject: [PATCH 002/213] update opengl notes --- README.md | 17 ++++++++++++++++- .../10.OpenGL ES\347\272\271\347\220\206.md" | 0 ...\255\346\224\276\350\247\206\351\242\221.md" | 0 .../12.OpenGL ES\346\273\244\351\225\234.md" | 0 .../5.GLTextureView\345\256\236\347\216\260.md" | 0 ...\266\344\270\211\350\247\222\345\275\242.md" | 0 ...\242\345\217\212\345\234\206\345\275\242.md" | 0 ...\345\231\250\350\257\255\350\250\200GLSL.md" | 0 ...47\261\273\345\217\212Matrix\347\261\273.md" | 0 9 files changed, 16 insertions(+), 1 deletion(-) rename "VideoDevelopment/OpenGL/8.OpenGL ES\347\272\271\347\220\206.md" => "VideoDevelopment/OpenGL/10.OpenGL ES\347\272\271\347\220\206.md" (100%) rename "VideoDevelopment/OpenGL/9.GLSurfaceView+MediaPlayer\346\222\255\346\224\276\350\247\206\351\242\221.md" => "VideoDevelopment/OpenGL/11.GLSurfaceView+MediaPlayer\346\222\255\346\224\276\350\247\206\351\242\221.md" (100%) rename "VideoDevelopment/OpenGL/10.OpenGL ES\346\273\244\351\225\234.md" => "VideoDevelopment/OpenGL/12.OpenGL ES\346\273\244\351\225\234.md" (100%) rename "VideoDevelopment/OpenGL/4.GLTextureView\345\256\236\347\216\260.md" => "VideoDevelopment/OpenGL/5.GLTextureView\345\256\236\347\216\260.md" (100%) rename "VideoDevelopment/OpenGL/4.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" => "VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" (100%) rename "VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" => "VideoDevelopment/OpenGL/7.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" (100%) rename "VideoDevelopment/OpenGL/6.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" => "VideoDevelopment/OpenGL/8.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" (100%) rename "VideoDevelopment/OpenGL/7.GLES\347\261\273\345\217\212Matrix\347\261\273.md" => "VideoDevelopment/OpenGL/9.GLES\347\261\273\345\217\212Matrix\347\261\273.md" (100%) diff --git a/README.md b/README.md index 929bf3f5..3c3e2f83 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,20 @@ Android学习笔记 - [CDN及PCDN][228] - [P2P][229] - [播放器性能优化][230] - + - [OpenGL][231] + - [1.OpenGL简介][232] + - [][] + - [][] + - [][] + - [][] + - [][] + - [][] + - [][] + - [][] + - [][] + - [][] + - [弹幕][] + - [][] - [图片加载][45] - [Glide简介(上)][25] - [Glide简介(下)][26] @@ -492,6 +505,8 @@ Android学习笔记 [228]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/CDN%E5%8F%8APCDN.md "CDN及PCDN" [229]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/P2P.md "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 "播放器性能优化" +[231]: hhttps://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简介.md" diff --git "a/VideoDevelopment/OpenGL/8.OpenGL ES\347\272\271\347\220\206.md" "b/VideoDevelopment/OpenGL/10.OpenGL ES\347\272\271\347\220\206.md" similarity index 100% rename from "VideoDevelopment/OpenGL/8.OpenGL ES\347\272\271\347\220\206.md" rename to "VideoDevelopment/OpenGL/10.OpenGL ES\347\272\271\347\220\206.md" diff --git "a/VideoDevelopment/OpenGL/9.GLSurfaceView+MediaPlayer\346\222\255\346\224\276\350\247\206\351\242\221.md" "b/VideoDevelopment/OpenGL/11.GLSurfaceView+MediaPlayer\346\222\255\346\224\276\350\247\206\351\242\221.md" similarity index 100% rename from "VideoDevelopment/OpenGL/9.GLSurfaceView+MediaPlayer\346\222\255\346\224\276\350\247\206\351\242\221.md" rename to "VideoDevelopment/OpenGL/11.GLSurfaceView+MediaPlayer\346\222\255\346\224\276\350\247\206\351\242\221.md" diff --git "a/VideoDevelopment/OpenGL/10.OpenGL ES\346\273\244\351\225\234.md" "b/VideoDevelopment/OpenGL/12.OpenGL ES\346\273\244\351\225\234.md" similarity index 100% rename from "VideoDevelopment/OpenGL/10.OpenGL ES\346\273\244\351\225\234.md" rename to "VideoDevelopment/OpenGL/12.OpenGL ES\346\273\244\351\225\234.md" diff --git "a/VideoDevelopment/OpenGL/4.GLTextureView\345\256\236\347\216\260.md" "b/VideoDevelopment/OpenGL/5.GLTextureView\345\256\236\347\216\260.md" similarity index 100% rename from "VideoDevelopment/OpenGL/4.GLTextureView\345\256\236\347\216\260.md" rename to "VideoDevelopment/OpenGL/5.GLTextureView\345\256\236\347\216\260.md" diff --git "a/VideoDevelopment/OpenGL/4.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" "b/VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" similarity index 100% rename from "VideoDevelopment/OpenGL/4.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" rename to "VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" diff --git "a/VideoDevelopment/OpenGL/5.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/7.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" similarity index 100% rename from "VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" rename to "VideoDevelopment/OpenGL/7.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" diff --git "a/VideoDevelopment/OpenGL/6.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" "b/VideoDevelopment/OpenGL/8.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" similarity index 100% rename from "VideoDevelopment/OpenGL/6.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" rename to "VideoDevelopment/OpenGL/8.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" diff --git "a/VideoDevelopment/OpenGL/7.GLES\347\261\273\345\217\212Matrix\347\261\273.md" "b/VideoDevelopment/OpenGL/9.GLES\347\261\273\345\217\212Matrix\347\261\273.md" similarity index 100% rename from "VideoDevelopment/OpenGL/7.GLES\347\261\273\345\217\212Matrix\347\261\273.md" rename to "VideoDevelopment/OpenGL/9.GLES\347\261\273\345\217\212Matrix\347\261\273.md" From ca8eb7b3bccf74007cde6979c27f032d29b926db Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 18 Mar 2020 16:30:27 +0800 Subject: [PATCH 003/213] add iamges --- README.md | 8 +++++--- ...er\346\222\255\346\224\276\350\247\206\351\242\221.md" | 0 .../OpenGL/11.OpenGL ES\346\273\244\351\225\234.md" | 0 .../OpenGL/4.GLTextureView\345\256\236\347\216\260.md" | 0 ...30\345\210\266\344\270\211\350\247\222\345\275\242.md" | 0 ...51\345\275\242\345\217\212\345\234\206\345\275\242.md" | 0 ...50\211\262\345\231\250\350\257\255\350\250\200GLSL.md" | 0 .../8.GLES\347\261\273\345\217\212Matrix\347\261\273.md" | 0 .../OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" | 0 9 files changed, 5 insertions(+), 3 deletions(-) rename "VideoDevelopment/OpenGL/11.GLSurfaceView+MediaPlayer\346\222\255\346\224\276\350\247\206\351\242\221.md" => "VideoDevelopment/OpenGL/10.GLSurfaceView+MediaPlayer\346\222\255\346\224\276\350\247\206\351\242\221.md" (100%) rename "VideoDevelopment/OpenGL/12.OpenGL ES\346\273\244\351\225\234.md" => "VideoDevelopment/OpenGL/11.OpenGL ES\346\273\244\351\225\234.md" (100%) rename "VideoDevelopment/OpenGL/5.GLTextureView\345\256\236\347\216\260.md" => "VideoDevelopment/OpenGL/4.GLTextureView\345\256\236\347\216\260.md" (100%) rename "VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" => "VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" (100%) rename "VideoDevelopment/OpenGL/7.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" => "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" (100%) rename "VideoDevelopment/OpenGL/8.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" => "VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" (100%) rename "VideoDevelopment/OpenGL/9.GLES\347\261\273\345\217\212Matrix\347\261\273.md" => "VideoDevelopment/OpenGL/8.GLES\347\261\273\345\217\212Matrix\347\261\273.md" (100%) rename "VideoDevelopment/OpenGL/10.OpenGL ES\347\272\271\347\220\206.md" => "VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" (100%) diff --git a/README.md b/README.md index 3c3e2f83..f14907ef 100644 --- a/README.md +++ b/README.md @@ -68,8 +68,8 @@ Android学习笔记 - [播放器性能优化][230] - [OpenGL][231] - [1.OpenGL简介][232] - - [][] - - [][] + - [2.GLSurfaceView简介][233] + - [3.GLSurfaceView源码解析][234] - [][] - [][] - [][] @@ -506,7 +506,9 @@ Android学习笔记 [229]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/P2P.md "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 "播放器性能优化" [231]: hhttps://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简介.md" +[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简介"" +[234]: 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源码解析" diff --git "a/VideoDevelopment/OpenGL/11.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" similarity index 100% rename from "VideoDevelopment/OpenGL/11.GLSurfaceView+MediaPlayer\346\222\255\346\224\276\350\247\206\351\242\221.md" rename to "VideoDevelopment/OpenGL/10.GLSurfaceView+MediaPlayer\346\222\255\346\224\276\350\247\206\351\242\221.md" diff --git "a/VideoDevelopment/OpenGL/12.OpenGL ES\346\273\244\351\225\234.md" "b/VideoDevelopment/OpenGL/11.OpenGL ES\346\273\244\351\225\234.md" similarity index 100% rename from "VideoDevelopment/OpenGL/12.OpenGL ES\346\273\244\351\225\234.md" rename to "VideoDevelopment/OpenGL/11.OpenGL ES\346\273\244\351\225\234.md" diff --git "a/VideoDevelopment/OpenGL/5.GLTextureView\345\256\236\347\216\260.md" "b/VideoDevelopment/OpenGL/4.GLTextureView\345\256\236\347\216\260.md" similarity index 100% rename from "VideoDevelopment/OpenGL/5.GLTextureView\345\256\236\347\216\260.md" rename to "VideoDevelopment/OpenGL/4.GLTextureView\345\256\236\347\216\260.md" diff --git "a/VideoDevelopment/OpenGL/6.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" similarity index 100% rename from "VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" rename to "VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" diff --git "a/VideoDevelopment/OpenGL/7.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" similarity index 100% rename from "VideoDevelopment/OpenGL/7.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" rename to "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" diff --git "a/VideoDevelopment/OpenGL/8.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" similarity index 100% rename from "VideoDevelopment/OpenGL/8.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" rename to "VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" diff --git "a/VideoDevelopment/OpenGL/9.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" similarity index 100% rename from "VideoDevelopment/OpenGL/9.GLES\347\261\273\345\217\212Matrix\347\261\273.md" rename to "VideoDevelopment/OpenGL/8.GLES\347\261\273\345\217\212Matrix\347\261\273.md" diff --git "a/VideoDevelopment/OpenGL/10.OpenGL ES\347\272\271\347\220\206.md" "b/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" similarity index 100% rename from "VideoDevelopment/OpenGL/10.OpenGL ES\347\272\271\347\220\206.md" rename to "VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" From 6067c8cdb654dc3567b46d5a5b637b1c556687fa Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 18 Mar 2020 17:06:28 +0800 Subject: [PATCH 004/213] update --- README.md | 32 +++++++++++++------ .../1.OpenGL\347\256\200\344\273\213.md" | 6 ++++ ...55\346\224\276\350\247\206\351\242\221.md" | 30 +++++++++++++++-- .../11.OpenGL ES\346\273\244\351\225\234.md" | 8 ++++- ....GLSurfaceView\347\256\200\344\273\213.md" | 15 ++++++--- ...20\347\240\201\350\247\243\346\236\220.md" | 24 +++++++++++++- ....GLTextureView\345\256\236\347\216\260.md" | 28 ++++++++++++++-- ...66\344\270\211\350\247\222\345\275\242.md" | 23 ++++++++++++- ...42\345\217\212\345\234\206\345\275\242.md" | 9 +++++- ...45\231\250\350\257\255\350\250\200GLSL.md" | 10 +++++- ...\261\273\345\217\212Matrix\347\261\273.md" | 11 ++++++- .../9.OpenGL ES\347\272\271\347\220\206.md" | 9 +++++- 12 files changed, 180 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index f14907ef..9ae58867 100644 --- a/README.md +++ b/README.md @@ -66,20 +66,21 @@ Android学习笔记 - [CDN及PCDN][228] - [P2P][229] - [播放器性能优化][230] + - [MediaExtractor、MediaCodec、MediaMuxer][245] - [OpenGL][231] - [1.OpenGL简介][232] - [2.GLSurfaceView简介][233] - [3.GLSurfaceView源码解析][234] - - [][] - - [][] - - [][] - - [][] - - [][] - - [][] - - [][] - - [][] - - [弹幕][] - - [][] + - [4.GLTextureView实现][235] + - [5.OpenGL ES绘制三角形][236] + - [6.OpenGL ES绘制矩形及圆形][237] + - [7.OpenGL ES着色器语言GLSL][238] + - [8.GLES类及Matrix类][239] + - [9.OpenGL ES纹理][240] + - [10.GLSurfaceView+MediaPlayer播放视频][241] + - [11.OpenGL ES滤镜][242] + - [弹幕][243] + - [Android弹幕实现][244] - [图片加载][45] - [Glide简介(上)][25] - [Glide简介(下)][26] @@ -509,6 +510,17 @@ Android学习笔记 [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简介"" [234]: 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源码解析" +[235]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/4.GLTextureView%E5%AE%9E%E7%8E%B0.md "4.GLTextureView实现.md" +[236]: 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绘制三角形" +[237]: 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 "6.OpenGL ES绘制矩形及圆形" +[238]: 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 "7.OpenGL ES着色器语言GLSL" +[239]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/8.GLES%E7%B1%BB%E5%8F%8AMatrix%E7%B1%BB.md "8.GLES类及Matrix类" +[240]: "https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/9.OpenGL%20ES%E7%BA%B9%E7%90%86.md" "9.OpenGL ES纹理" +[241]: 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播放视频" +[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" 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 0c7f6f70..76c0a47f 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" @@ -6,6 +6,7 @@ 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 ES @@ -228,8 +229,13 @@ Android 框架中有如下两个基本类,用于通过 OpenGL ES API 来创建 + +[下一篇: 2.GLSurfaceView简介](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/2.GLSurfaceView%E7%AE%80%E4%BB%8B.md) +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! 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 b6682af6..2af47100 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" @@ -1,4 +1,4 @@ -## 9.GLSurfaceView+MediaPlayer播放视频 +## 10.GLSurfaceView+MediaPlayer播放视频 @@ -105,4 +105,30 @@ public class VideoActivity extends Activity { -### 增加OpenGL ES \ No newline at end of file +### 增加OpenGL ES + + + + + + + + +[上一篇: 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) + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + + + + + + + + + + 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 026edc95..4a959561 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" @@ -1,4 +1,4 @@ -## 10.OpenGL ES滤镜 +## 11.OpenGL ES滤镜 滤镜其实就是利用纹理。 @@ -27,6 +27,12 @@ RGB三个通道的颜色取反,而alpha通道不变。 +[上一篇: 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/2.GLSurfaceView\347\256\200\344\273\213.md" "b/VideoDevelopment/OpenGL/2.GLSurfaceView\347\256\200\344\273\213.md" index d3d7b314..aca97c0b 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" @@ -36,10 +36,11 @@ GLSurfaceView继承自SurfaceView,它主要是在SurfaceView的基础上加入 OpenGL ES 3.0/3.1 API 软件包 -- ``` - android.opengl: 此软件包提供了 OpenGL ES 3.0/3.1 类的接口。版本 3.0 从 Android 4.3(API 级别 18)开始可用。版本 3.1 从 Android 5.0(API 级别 21)开始可用。 - ``` - +- + android.opengl: 此软件包提供了 OpenGL ES 3.0/3.1 类的接口。 + 版本 3.0 从 Android 4.3(API 级别 18)开始可用。 + 版本 3.1 从 Android 5.0(API 级别 21)开始可用。 + - `GLES30` - `GLES31` - `GLES31Ext` ([Android Extension Pack](https://developer.android.google.cn/guide/topics/graphics/opengl#aep)) @@ -198,7 +199,13 @@ SurfaceTexture对象可以在任何线程上创建。 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) + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! 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 c4c11e8b..e2659c45 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" @@ -1,4 +1,4 @@ -GLSurfaceView源码解析 +## 3.GLSurfaceView源码解析 我感觉还是先看一下源码,了解一下内部的流程,再接着学习其他的OpenGL部分会更合适。 @@ -688,3 +688,25 @@ public interface Renderer { 这个类主要提供的是线程同步控制的功能,因为在GLSurfaceView里面有两个线程: GL线程和调用线程。所以对一些变量必须要进行同步。 + + + +[上一篇: 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) + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + + + + + + + + + + + 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 6f5b93fe..661e256f 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 @@ -### GLTextureView +### 4.GLTextureView 系统提供了GLSurfaceView,确没有提供GLTextureView,但是我们目前的项目灰常复杂庞大,我不想去封一层接口,然后动态去选择使用GLSurfaceView(实现一些纹理效果)或者TextureView(无OpenGL效果)来播放视频。我只想在目前的基础上去扩展,我想去实现一个GLTextureView。上面分析完GLSurfaceView的源码后我们也可以自己去实现GLTextureView的功能了。具体的实现就是和GLSurfaceView的源码一模一样。 @@ -1971,4 +1971,28 @@ public class GLTextureView extends TextureView implements TextureView.SurfaceTex private int mEGLContextClientVersion; private boolean mPreserveEGLContextOnPause; } -``` \ No newline at end of file +``` + + + + + + +[上一篇: 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) + +--- + +- 邮箱 :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 5532ef7d..e14647ae 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,4 +1,4 @@ -## OpenGL Es绘制三角形 +## 5.OpenGL Es绘制三角形 OpenGL ES的绘制需要有一下步骤: @@ -861,3 +861,24 @@ class TriangleRender implements GLSurfaceView.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) + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + + + + + + + + + + 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 581c449c..3c40dc74 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,4 +1,4 @@ -## 5.OpenGL ES绘制矩形及圆形 +## 6.OpenGL ES绘制矩形及圆形 @@ -500,6 +500,13 @@ public class CircularRender implements GLSurfaceView.Renderer { ``` +[上一篇: 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) + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! 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 a286783e..0dd05b65 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,4 +1,4 @@ -## 6.OpenGL ES着色器语言GLSL +## 7.OpenGL ES着色器语言GLSL 顶点着色器: @@ -205,6 +205,14 @@ GLSL的类型转换与C不同。在GLSL中类型不可以自动提升,比如fl +[上一篇: 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) + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! + 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 72141be7..a8b903a6 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" @@ -1,4 +1,4 @@ -## 7.GLES类及Matrix类 +## 8.GLES类及Matrix类 @@ -136,6 +136,15 @@ GLES20.glActiveTexture()确定了后续的纹理状态改变影响哪个纹理 +[上一篇: 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) + +--- + +- 邮箱 :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 f7c28838..c712f597 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" @@ -1,4 +1,4 @@ -## OpenGL ES纹理 +## 9.OpenGL ES纹理 @@ -528,6 +528,13 @@ public class TextureHelper { +[上一篇: 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) + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! From b82bcd94c18df151d783f43a680450108f7ecf00 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 18 Mar 2020 17:25:20 +0800 Subject: [PATCH 005/213] update --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9ae58867..409e491a 100644 --- a/README.md +++ b/README.md @@ -506,17 +506,17 @@ Android学习笔记 [228]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/CDN%E5%8F%8APCDN.md "CDN及PCDN" [229]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/P2P.md "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 "播放器性能优化" -[231]: hhttps://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简介"" +[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简介" [234]: 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源码解析" -[235]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/4.GLTextureView%E5%AE%9E%E7%8E%B0.md "4.GLTextureView实现.md" +[235]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/4.GLTextureView%E5%AE%9E%E7%8E%B0.md "4.GLTextureView实现.md" [236]: 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绘制三角形" [237]: 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 "6.OpenGL ES绘制矩形及圆形" [238]: 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 "7.OpenGL ES着色器语言GLSL" [239]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/8.GLES%E7%B1%BB%E5%8F%8AMatrix%E7%B1%BB.md "8.GLES类及Matrix类" [240]: "https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/9.OpenGL%20ES%E7%BA%B9%E7%90%86.md" "9.OpenGL ES纹理" -[241]: 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播放视频" +[241]: 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播放视频" [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弹幕实现" From 7ddae18873f0fd5407370550f63c619de7dd82e4 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 18 Mar 2020 19:58:30 +0800 Subject: [PATCH 006/213] update --- .../Git\347\256\200\344\273\213.md" | 1 - README.md | 2 +- .../1.OpenGL\347\256\200\344\273\213.md" | 8 +-- ....GLSurfaceView\347\256\200\344\273\213.md" | 47 ++++++----------- ...20\347\240\201\350\247\243\346\236\220.md" | 50 +++++++++---------- ....GLTextureView\345\256\236\347\216\260.md" | 4 +- ...66\344\270\211\350\247\222\345\275\242.md" | 17 +++---- 7 files changed, 55 insertions(+), 74 deletions(-) diff --git "a/JavaKnowledge/Git\347\256\200\344\273\213.md" "b/JavaKnowledge/Git\347\256\200\344\273\213.md" index b77fac3c..f98977be 100644 --- "a/JavaKnowledge/Git\347\256\200\344\273\213.md" +++ "b/JavaKnowledge/Git\347\256\200\344\273\213.md" @@ -19,7 +19,6 @@ Git简介 和集中式版本控制系统相比,分布式版本控制系统的安全性要高很多,因为每个人电脑里都有完整的版本库,某一个 人 的电脑坏掉了不要紧,随便从其他人那里复制一个就可以了。而集中式版本控制系统的中央服务器要是出了问题,所有人都没法干活了。 在实际使用分布式版本控制系统的时候,其实很少在两人之间的电脑上推送版本库的修改,因为可能你们俩不在一个局域网内,两台电脑互相访问不了,也可能今天你的同事病了,他的电脑压根没有开机。因此,分布式版本控制系统通常也有一台充当“中央服务器”的电脑,但这个服务器的作用仅仅是用来方便“交换”大家的修改,没有它大家也一样干活,只是交换修改不方便而已。 ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_fenbu.jpeg) -dddd 版本库 --- diff --git a/README.md b/README.md index 409e491a..82ccb40e 100644 --- a/README.md +++ b/README.md @@ -515,7 +515,7 @@ Android学习笔记 [237]: 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 "6.OpenGL ES绘制矩形及圆形" [238]: 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 "7.OpenGL ES着色器语言GLSL" [239]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/8.GLES%E7%B1%BB%E5%8F%8AMatrix%E7%B1%BB.md "8.GLES类及Matrix类" -[240]: "https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/9.OpenGL%20ES%E7%BA%B9%E7%90%86.md" "9.OpenGL ES纹理" +[240]: "https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/9.OpenGL%20ES%E7%BA%B9%E7%90%86.md "9.OpenGL ES纹理" [241]: 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播放视频" [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 "弹幕" 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 76c0a47f..bbd77b44 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,8 +1,8 @@ ## 1.OpenGL简介 -[OpenGL官网](https://www.opengl.org/) +[OpenGL官网](https://www.opengl.org/) -OpenGL(Open Graphics Library开发图形库)是用于渲染2D、3D矢量图形的跨语言、跨平台的应用程序编程接口。 +OpenGL(Open Graphics Library开放图形库)是用于渲染2D、3D矢量图形的跨语言、跨平台的应用程序编程接口。 OpenGL的前身是硅谷图形功能(SGI)公司为其图形工作站开发的IRIS GL,后来因为IRIS GL的移植性不好,所以在其基础上,开发出了OpenGl。OpenGl一般用于在图形工作站,PC端使用,由于性能各方面原因,在移动端使用OpenGl基本带不动。为此,Khronos公司就为OpenGl提供了一个子集,OpenGl ES(OpenGl for Embedded System)。 @@ -11,7 +11,7 @@ OpenGL的前身是硅谷图形功能(SGI)公司为其图形工作站开发的IRI ### OpenGL ES -[OpenGL ES 官网](https://www.khronos.org/opengles/) +[OpenGL ES 官网](https://www.khronos.org/opengles/) OpenGL ES是免费的跨平台的功能完善的2D/3D图形库接口的API,是OpenGL的一个子集,主要针对手机、Pad和游戏主机等嵌入式设备而设计。 @@ -174,7 +174,7 @@ OpenGL是一个仅仅关注图像渲染的图像接口库,在渲染过程中 EGL为双缓冲工作模式,既有一个Back Frame Buffer和一个Front Frame Buffer,正常绘制的目标都是Back Frame Buffer,绘制完成后再调用eglSwapBuffer API,将绘制完毕的FrameBuffer交换到Front Frame Buffer并显示出来。 -要在Android平台实现OpenGL渲染,需要完成一系列的EGL操作,主要为下面几步: +要在Android平台实现OpenGL渲染,需要完成一系列的EGL操作,主要为下面几步(后面分析GLSurfaceView源码的时候也是这样来实现的): 1. 获取显示设备(EGL Display) 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 aca97c0b..dab69c89 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" @@ -2,9 +2,9 @@ Android 通过其框架 API 和原生开发套件 (NDK) 来支持 OpenGL。 -Android 框架中有如下两个基本类,用于通过 OpenGL ES API 来创建和操控图形:`GLSurfaceView` 和 `GLSurfaceView.Renderer`。也就是说我们想在Android中使用OpenGL ES的最简单的方法就是将我们要显示的图像渲染到GLSurfaceView上,但它只是一个展示类,至于怎么渲染到上面我们就要自己去实现GLSurfaceView.Renderer来生成我们自己的渲染器从而完成渲染操作。 +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)。 -######## GLSurfaceView(使用OpenGL绘制的图形的视图容器) +### GLSurfaceView(使用OpenGL绘制的图形的视图容器) SurfaceView在View的基础上创建了独立的Surface,拥有SurfaceHolder来管理它的Surface,渲染的工作可以不再主线程中做。可以通过SurfaceHolder得到Canvas,在单独的线程中,利用Canvas绘制需要显示的内容,然后更新到Surface上。 @@ -16,39 +16,28 @@ GLSurfaceView继承自SurfaceView,它主要是在SurfaceView的基础上加入 - onPause:暂停渲染,最好在Activity、Fragment的onPause方法内调用,减少不必要的性能开销,避免不必要的崩溃 - onResume:恢复渲染 - setRender:设置渲染器 -- setRenderMode:设置渲染模式 +- setRenderMode:设置渲染模式,有连续渲染和按需渲染两种模式,按需渲染需要调用下面的requestRender方法才会去渲染 - requestRender:请求渲染,请求异步线程进行渲染,调用后不会立即进行渲染。渲染会回调到Renderer接口的onDrawFrame()方法 - queueEvent:插入一个Runnable任务到后台渲染线程上执行。相应的,渲染线程中可以通过Activity的runOnUIThread的方法来传递事件给主线程去执行 #### GLSurfaceView.Renderer(可控制该视图中绘制的图形) -此接口定义了在 `GLSurfaceView` 中绘制图形所需的方法。您必须将此接口的一个实现作为单独的类提供,并使用 `GLSurfaceView.setRenderer()` 将其附加到您的 `GLSurfaceView` 实例。 +此接口定义了在GLSurfaceView中绘制图形所需的方法。您必须将此接口的一个实现作为单独的类提供,并使用GLSurfaceView.setRenderer()将其附加到您的GLSurfaceView实例。 -`GLSurfaceView.Renderer` 接口要求您实现以下方法: +GLSurfaceView.Renderer接口要求您实现以下方法: +- onSurfaceCreated():系统会在创建GLSurface时调用一次此方法。使用此方法可执行仅需发生一次的操作,例如设置OpenGL环境参数或初始化OpenGL图形对象。 +- onDrawFrame():完成绘制工作,每一帧图像的渲染都要在这里完成,系统会在每次重新绘制GLSurfaceView时调用此方法。请将此方法作为绘制(和重新绘制)图形对象的主要执行点。 +- onSurfaceChanged():系统会在GLSurfaceView几何图形发生变化(包括GLSurfaceView大小发生变化或设备屏幕方向发生变化)时调用此方法。例如,系统会在设备屏幕方向由纵向变为横向时调用此方法。使用此方法可响应GLSurfaceView容器中的更改。 -- `onSurfaceCreated()`:系统会在创建 `GLSurfaceView` 时调用一次此方法。使用此方法可执行仅需发生一次的操作,例如设置 OpenGL 环境参数或初始化 OpenGL 图形对象。 -- `onDrawFrame()`:完成绘制工作,每一帧图像的渲染都要在这里完成,系统会在每次重新绘制 `GLSurfaceView` 时调用此方法。请将此方法作为绘制(和重新绘制)图形对象的主要执行点。 -- `onSurfaceChanged()`:系统会在 `GLSurfaceView` 几何图形发生变化(包括 `GLSurfaceView` 大小发生变化或设备屏幕方向发生变化)时调用此方法。例如,系统会在设备屏幕方向由纵向变为横向时调用此方法。使用此方法可响应 `GLSurfaceView` 容器中的更改。 +使用GLSurfaceView和GLSurfaceView.Renderer为OpenGL ES建立容器视图后,您便可以开始使用以下类调用 OpenGL API: - - -使用 `GLSurfaceView` 和 `GLSurfaceView.Renderer` 为 OpenGL ES 建立容器视图后,您便可以开始使用以下类调用 OpenGL API: - -OpenGL ES 3.0/3.1 API 软件包 - -- - android.opengl: 此软件包提供了 OpenGL ES 3.0/3.1 类的接口。 - 版本 3.0 从 Android 4.3(API 级别 18)开始可用。 - 版本 3.1 从 Android 5.0(API 级别 21)开始可用。 - +OpenGL ES 3.0/3.1 API 软件包: +- android.opengl: 此软件包提供了 OpenGL ES 3.0/3.1 类的接口。 - `GLES30` - `GLES31` - `GLES31Ext` ([Android Extension Pack](https://developer.android.google.cn/guide/topics/graphics/opengl#aep)) - - -[SurfaceView与TextureView](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/SurfaceView%E4%B8%8ETextureView.md) - +### GLSurfaceView类 ```java package android.opengl; /** @@ -60,13 +49,9 @@ package android.opengl; ``` -GPU加速:GLSurfaceView的效率是SurfaceView的30倍以上,SurfaceView使用画布进行绘制,GLSurfaceView利用GPU加速提高了绘制效率。 -View的绘制onDraw(Canvas canvas)使用Skia渲染引擎渲染,而GLSurfaceView的渲染器Renderer的onDrawFrame(GL10 gl)使用opengl绘制引擎进行渲染。 - - +GPU加速:GLSurfaceView的效率是SurfaceView的30倍以上,SurfaceView使用画布进行绘制,GLSurfaceView利用GPU加速提高了绘制效率。View的绘制onDraw(Canvas canvas)使用Skia渲染引擎渲染,而GLSurfaceView的渲染器Renderer的onDrawFrame(GL10 gl)使用OpenGL绘制引擎进行渲染。 GLSurfaceView的特性: - - 管理一个surface,这个surface就是一块特殊的内存,能直接排版到android的视图view上。 - 管理一个EGL Display,它能让OpenGL把内容渲染到surface上 - 用户自定义渲染器render @@ -82,13 +67,13 @@ GLSurfaceView的特性: 首选现在AndroidManifest.xml文件中声明使用OpenGL ES的版本: -``` +```xml ``` -``` +```java public class MainActivity extends AppCompatActivity { private GLSurfaceView mGlSurfaceView; @@ -140,7 +125,7 @@ Render接口重写的三个方法中调用了GLES31的API方法: #### RenderMode -GLSurfaceView默认采用的是RENDERMODE_CONTINUOUSLY连续渲染的方式,刷新的帧率是60FPS,16ms就重绘一次,可以通过mGLView.setRenderMode()更改其渲染方式为RENDERMODE_WHEN_DIRTY,表示被动渲染,在surfaceCreate的时候会绘制一次,之后只有在调用requestRender或者onResume等方法主动请求重绘时才会进行渲染,如果你的界面不需要频繁的刷新最好使用RENDERMODE_WHEN_DIRTY,这样可以降低CPU和GPU的活动。 +GLSurfaceView默认采用的是RENDERMODE_CONTINUOUSLY连续渲染的方式,刷新的帧率是60FPS,16ms就重绘一次,可以通过mGLView.setRenderMode()更改其渲染方式为RENDERMODE_WHEN_DIRTY,表示按需渲染,在surfaceCreate的时候会绘制一次,之后只有在调用requestRender或者onResume等方法主动请求重绘时才会进行渲染,如果你的界面不需要频繁的刷新最好使用RENDERMODE_WHEN_DIRTY,这样可以降低CPU和GPU的活动。 #### 状态处理 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 e2659c45..d9cbc25a 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" @@ -8,11 +8,11 @@ ```java public interface Renderer { - // surface创建的回调 + // surface创建的回调 void onSurfaceCreated(GL10 gl, EGLConfig config); - // surface大小改变的回调 + // surface大小改变的回调 void onSurfaceChanged(GL10 gl, int width, int height); - // 开始绘制每一帧的回调 + // 开始绘制每一帧的回调 void onDrawFrame(GL10 gl); } ``` @@ -20,26 +20,26 @@ public interface Renderer { - setRenderer()方法 ```java - public void setRenderer(Renderer renderer) { - // 保证setRenderer方法只能被调用一次 - checkRenderThreadState(); - if (mEGLConfigChooser == null) { - // 设置EGLConfgi,如果不设置就用默认的一个RGB_888的Surface选择器 - mEGLConfigChooser = new SimpleEGLConfigChooser(true); - } - if (mEGLContextFactory == null) { - // 设置EGLContext工厂,如果不设置就用默认的EGLContext的工厂类,用他来创建EGLContext - mEGLContextFactory = new DefaultContextFactory(); - } - if (mEGLWindowSurfaceFactory == null) { - // 设置EGLSurface工厂,如果不设置就用默认的EGLSurface的工厂类,我们可以通过这个方法来设置让图像渲染到其他地方的surface上,例如textureview上 - mEGLWindowSurfaceFactory = new DefaultWindowSurfaceFactory(); - } - mRenderer = renderer; - // 创建并开启GLThread,GL线程 - mGLThread = new GLThread(mThisWeakRef); - mGLThread.start(); - } +public void setRenderer(Renderer renderer) { + // 保证setRenderer方法只能被调用一次 + checkRenderThreadState(); + if (mEGLConfigChooser == null) { + // 设置EGLConfgi,如果不设置就用默认的一个RGB_888的Surface选择器 + mEGLConfigChooser = new SimpleEGLConfigChooser(true); + } + if (mEGLContextFactory == null) { + // 设置EGLContext工厂,如果不设置就用默认的EGLContext的工厂类,用他来创建EGLContext + mEGLContextFactory = new DefaultContextFactory(); + } + if (mEGLWindowSurfaceFactory == null) { + // 设置EGLSurface工厂,如果不设置就用默认的EGLSurface的工厂类,我们可以通过这个方法来设置让图像渲染到其他地方的surface上,例如textureview上 + mEGLWindowSurfaceFactory = new DefaultWindowSurfaceFactory(); + } + mRenderer = renderer; + // 创建并开启GLThread,GL线程 + mGLThread = new GLThread(mThisWeakRef); + mGLThread.start(); +} ``` - 接下来看一下GLThread的线程的实现,GLThread是GLSurfaceView自带的一个渲染线程,同步的,不会阻塞线程,主要用来执行OpenGL的绘制工作: @@ -50,7 +50,7 @@ public interface Renderer { super(); mWidth = 0; mHeight = 0; - // 主动渲染模式下是true,如果是被动渲染模式就为false,然后通过requesetRender()方法来修改它的值 + // 连续渲染模式下是true,如果是按需渲染模式就为false,然后通过requesetRender()方法来修改它的值 mRequestRender = true; // 默认的渲染模式 mRenderMode = RENDERMODE_CONTINUOUSLY; @@ -691,7 +691,7 @@ public interface 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) --- 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 661e256f..195b9e83 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的源码一模一样。 @@ -1978,7 +1978,7 @@ 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) --- 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 e14647ae..dc8c01dd 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,6 +1,6 @@ -## 5.OpenGL Es绘制三角形 +## 5.OpenGL ES绘制三角形 -OpenGL ES的绘制需要有一下步骤: +OpenGL ES的绘制需要有以下步骤: - 顶点输入 @@ -20,9 +20,9 @@ OpenGL ES的绘制需要有一下步骤: 每个着色器都起始于一个版本声明。OpenGL 3.3以及和更高版本中,GLSL版本号和OpenGL的版本是匹配的(比如说GLSL 420版本对应于OpenGL 4.2)。我们同样明确表示我们会使用核心模式。 - 下一步,使用`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)你后面会看到为什么我们会需要这个位置值。 - 为了设置顶点着色器的输出,我们必须把位置数据赋值给预定义的gl_Position变量,它在幕后是`vec4`类型的。在main函数的最后,我们将gl_Position设置的值会成为该顶点着色器的输出。由于我们的输入是一个3分量的向量,我们必须把它转换为4分量的。我们可以把`vec3`的数据作为`vec4`构造器的参数,同时把`w`分量设置为`1.0f`(我们会在后面解释为什么)来完成这一任务。 + 为了设置顶点着色器的输出,我们必须把位置数据赋值给预定义的gl_Position变量,它在幕后是vec4类型的。在main函数的最后,我们将gl_Position设置的值会成为该顶点着色器的输出。由于我们的输入是一个3分量的向量,我们必须把它转换为4分量的。我们可以把vec3的数据作为vec4构造器的参数,同时把w分量设置为1.0f(我们会在后面解释为什么)来完成这一任务。 - 编译着色器 @@ -34,16 +34,13 @@ OpenGL ES的绘制需要有一下步骤: ```glsl #version 330 core - out vec4 color; - - void main() - { + void main() { color = vec4(1.0f, 0.5f, 0.2f, 1.0f); } ``` - 片段着色器只需要一个输出变量,这个变量是一个4分量向量,它表示的是最终的输出颜色,我们应该自己将其计算出来。我们可以用`out`关键字声明输出变量,这里我们命名为color。下面,我们将一个alpha值为1.0(1.0代表完全不透明)的橘黄色的`vec4`赋值给颜色输出。之后也是需要编译着色器。 + 片段着色器只需要一个输出变量,这个变量是一个4分量向量,它表示的是最终的输出颜色,我们应该自己将其计算出来。我们可以用out关键字声明输出变量,这里我们命名为color。下面,我们将一个alpha值为1.0(1.0代表完全不透明)的橘黄色的vec4赋值给颜色输出。之后也是需要编译着色器。 - 着色器程序(Shader Program Object) @@ -61,7 +58,7 @@ OpenGL ES的绘制需要有一下步骤: - 链接顶点属性 - 顶点着色器允许我们指定任何以顶点属性为形式的输入。这使其具有很强的灵活性的同时,它还的确意味着我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。所以,我们必须在渲染前指定OpenGL该如何解释顶点数据。 + 顶点着色器允许我们指定任何以顶点属性为形式的输入。这使其具有很强的灵活性的同时,它还意味着我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。所以,我们必须在渲染前指定OpenGL该如何解释顶点数据。 我们的顶点缓冲数据会被解析成下面的样子: From 973889f433aec163654e36fa4b13f19c0d07112f Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 19 Mar 2020 21:37:25 +0800 Subject: [PATCH 007/213] update --- .../1.OpenGL\347\256\200\344\273\213.md" | 13 +-- ...55\346\224\276\350\247\206\351\242\221.md" | 2 +- .../11.OpenGL ES\346\273\244\351\225\234.md" | 2 +- ...66\344\270\211\350\247\222\345\275\242.md" | 22 +++-- ...42\345\217\212\345\234\206\345\275\242.md" | 2 +- ...45\231\250\350\257\255\350\250\200GLSL.md" | 61 +++++++----- ...\261\273\345\217\212Matrix\347\261\273.md" | 97 ++++++++++++++++++- .../9.OpenGL ES\347\272\271\347\220\206.md" | 26 ++++- 8 files changed, 178 insertions(+), 47 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 bbd77b44..3dc28da4 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,7 +53,9 @@ OpenGL ES 3.0兼容了2.0,并丰富了更多功能。 OpenGL渲染管线的流程为: 顶点数据 -> 顶点着色器 -> 图元装配 -> 几何着色器 -> 光栅化 -> 片段着色器 -> 逐片段处理 -> 帧缓冲 -如下图,阴影的表示OpenGL ES 3.0管线中可编程阶段。 +OpenGL ES 3.0实现了具有可编程着色功能的图形管线,有两个规范组成: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) @@ -65,7 +67,6 @@ OpenGL渲染管线的流程为: 顶点数据 -> 顶点着色器 -> 图元装配 着色器(shader)是在GPU上运行的小程序。从名称可以看出,可通过处理它们来处理顶点。此程序使用OpenGL ES SL语言来编写。它是一个描述顶点或像素特性的简单程序。OpenGL最本质的概念之一就是着色器,它是图形硬件设备所执行的一类特殊函数。理解着色器最好的办法就是把它看做是专为图形处理单元(即GPU)编译的一种小型程序。任何一种OpenGL程序本质上都可以被分为两部分:CPU端运行的部分,采用C++、Java之类的语言编写;以及GPU端运行的部分,使用GLSL语言编写。 - 顶点着色器可以操作的属性有: 位置、颜色、纹理坐标,但是不能创建新的顶点。最终产生纹理坐标、颜色、点位置等信息送往后续阶段。 - 图元装配(Primitive Assembly) @@ -170,6 +171,8 @@ OpenGL是一个仅仅关注图像渲染的图像接口库,在渲染过程中 ### EGL +OpenGL ES API没有提及如何创建渲染上下文,或者渲染上下文如何连接到原生窗口系统。EGL是Khronos渲染API(如OpenGL ES)和原生窗口系统之间的接口。 + 在OpenGL的设计中,OpenGL是不负责管理窗口的,窗口的管理交由各个设备自己完成。具体的来说就是iOS平台上使用EAGL提供本地平台对OpenGL的实现,在Android平台上使用EGL提供本地平台对OpenGL的实现。OpenGL其实是通过GPU进行渲染。但是我们的程序是运行在CPU上,要与GPU关联,这就需要通过EGL,它相当于Android上层应用于GPU通讯的中间层。 EGL为双缓冲工作模式,既有一个Back Frame Buffer和一个Front Frame Buffer,正常绘制的目标都是Back Frame Buffer,绘制完成后再调用eglSwapBuffer API,将绘制完毕的FrameBuffer交换到Front Frame Buffer并显示出来。 @@ -200,9 +203,7 @@ EGL为双缓冲工作模式,既有一个Back Frame Buffer和一个Front Frame eglMakeCurrent(EGLDisplay display, EGLSurface draw, EGLSurface read, EGLContext context);该方法的参数意义很明确,该方法在异步线程中被调用,该线程也会被成为GL线程,一旦设定后,所有OpenGL渲染相关的操作都必须放在该线程中执行。 - 通过上述操作,就完成了EGL的初始化设置,便可以进行OpenGL的渲染操作。 - - +通过上述操作,就完成了EGL的初始化设置,便可以进行OpenGL的渲染操作。所有EGL命令都是以egl前缀开始,对组成命令名的每个单词使用首字母大写(如eglCreateWindowSurface)。 #### OpenGL纹理 @@ -229,7 +230,7 @@ Android 框架中有如下两个基本类,用于通过 OpenGL ES API 来创建 - + [下一篇: 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 2af47100..6c845636 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" @@ -114,7 +114,7 @@ public class VideoActivity extends Activity { -[上一篇: 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) --- 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 4a959561..dc1cc40d 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" @@ -27,7 +27,7 @@ RGB三个通道的颜色取反,而alpha通道不变。 -[上一篇: 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) --- 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 dc8c01dd..7b859d6d 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" @@ -9,7 +9,7 @@ OpenGL ES的绘制需要有以下步骤: - 顶点着色器 ```glsl - #version 330 core + #version 300 core layout (location = 0) in vec3 position; void main() @@ -22,7 +22,9 @@ OpenGL ES的绘制需要有以下步骤: 下一步,使用in关键字,在顶点着色器中声明所有的输入顶点属性(Input Vertex Attribute)。现在我们只关心位置(Position)数据,所以我们只需要一个顶点属性。GLSL有一个向量数据类型,它包含1到4个float分量,包含的数量可以从它的后缀数字看出来。由于每个顶点都有一个3D坐标,我们就创建一个vec3输入变量position。我们同样也通过layout (location = 0)设定了输入变量的位置值(Location)你后面会看到为什么我们会需要这个位置值。 - 为了设置顶点着色器的输出,我们必须把位置数据赋值给预定义的gl_Position变量,它在幕后是vec4类型的。在main函数的最后,我们将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变量中输出一个位置。这个变量定义传递给管线下一个阶段的位置。 - 编译着色器 @@ -33,14 +35,14 @@ OpenGL ES的绘制需要有以下步骤: 片断着色器全是关于计算你的像素最后的颜色输出。颜色使用RGBA。 ```glsl - #version 330 core + #version 300 core out vec4 color; void main() { color = vec4(1.0f, 0.5f, 0.2f, 1.0f); } ``` - 片段着色器只需要一个输出变量,这个变量是一个4分量向量,它表示的是最终的输出颜色,我们应该自己将其计算出来。我们可以用out关键字声明输出变量,这里我们命名为color。下面,我们将一个alpha值为1.0(1.0代表完全不透明)的橘黄色的vec4赋值给颜色输出。之后也是需要编译着色器。 + 片段着色器只需要一个输出变量,这个变量是一个4分量向量,它表示的是最终的输出颜色,我们应该自己将其计算出来。我们可以用out关键字声明输出变量,这里我们命名为color。下面,我们将一个alpha值为1.0(1.0代表完全不透明)的橘黄色的vec4赋值给颜色输出。之后也是需要编译着色器。片段着色器声明的这个输出变量color的值会被输出到颜色缓冲区。,然后颜色缓冲区再通过EGL窗口显示。 - 着色器程序(Shader Program Object) @@ -54,7 +56,7 @@ OpenGL ES的绘制需要有以下步骤: glLinkProgram(shaderProgram); ``` - 链接完后需要使用glUseProgram方法,用刚创建的程序对象作为参数,以激活这个程序对象。 + 链接完后需要使用glUseProgram方法,用刚创建的程序对象作为参数,以激活这个程序对象。调用glUserProgram方法后,所有后续的渲染将用连接到这个程序对象的顶点和片段着色器进行。 - 链接顶点属性 @@ -84,12 +86,13 @@ OpenGL ES的绘制需要有以下步骤: 下面需要实现GLSurfaceView.Render接口,实现要绘制的部分: +- 用EGL创建屏幕上的渲染表面(GLSurfaceView内部实现) - 写顶点着色器和片段着色器文件。 - 加载编译顶点着色器和片段着色器。 - 确定需要绘制图形的坐标和颜色数据。 - 创建program对象,连接顶点和片断着色器,将坐标数据、颜色数据传到OpenGL ES程序中。] - 设置视图窗口(viewport)。 -- 使颜色缓冲区的内容显示到屏幕上。 +- 使颜色缓冲区的内容显示到EGL窗口上。 @@ -252,7 +255,7 @@ class MyGLRenderer implements GLSurfaceView.Renderer { } public void onDrawFrame(GL10 unused) { /**********6.绘制************/ - //把颜色缓冲区设置为我们预设的颜色 + //把颜色缓冲区设置为我们预设的颜色,绘图设计到多种缓冲区类型:颜色、深度和模板。这里只是向颜色缓冲区中绘制图形 GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT); //绑定vertex坐标数据,告诉OpenGL可以在缓冲区vertextBuffer中获取数据 @@ -262,7 +265,7 @@ class MyGLRenderer implements GLSurfaceView.Renderer { //准备颜色数据 /** - * glVertexAttribPointer()方法的参数分别为: + * 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); @@ -860,8 +863,7 @@ class TriangleRender implements GLSurfaceView.Renderer { - -[上一篇: 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) [下一篇: 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 3c40dc74..be35d31c 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" @@ -500,7 +500,7 @@ public class CircularRender implements GLSurfaceView.Renderer { ``` -[上一篇: 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) [下一篇: 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 0dd05b65..45549a14 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,6 +1,6 @@ ## 7.OpenGL ES着色器语言GLSL -顶点着色器: +### 顶点着色器: ``` #version 330 core @@ -15,7 +15,24 @@ void main() } ``` -片段着色器: +顶点着色器的输入包括: + +- 属性:用顶点数组提供的逐顶点数据 +- 统一变量和统一变量缓冲区:顶点着色器使用的不变数据 +- 采样器:代表顶点着色器使用的纹理的特殊统一变量类型 +- 着色器程序:顶点着色器程序源代码或者描述在操作顶点的可执行文件 + +顶点着色器的输出称作顶点着色器输出变量。在图元光栅化阶段,为每个生成的片段计算这些变量,并作为片段着色器的输入传入。 + +内建变量包括: + +- gl_VertexID: 一个输入变量,用于保存顶点的整数索引。 +- gl_InstanceID:一个输入变量,用于保存实例化绘图调用中图元的实例编号。 +- gl_Position:用于输出顶点位置的裁剪坐标。 +- gl_PointSize:用于写入以像素标示的点尺寸。 +- gl_FrontFacing:一个特殊变量,但不是由顶点着色器直接写入的,而是根据顶点着色器生成的位置值和渲染的图元类型生成的。 + +### 片段着色器: ``` #version 330 core @@ -29,7 +46,16 @@ void main() } ``` +片段着色器为片段操作提供了通用功能的可编程方法。片段着色器的输入由以下部分组成: +- 输入(或者可变值):顶点着色器生成的插值数据。顶点着色器输出跨图元进行插值,并作为输入传递给片段着色器。 +- 统一变量:片段着色器使用的状态,这些常量值在每个片段上不会变化。 +- 采样器:用于访问着色器中的纹理图像。 +- 代码:片段着色器源代码或者二进制代码,描述在片段上执行的操作。 + +片段着色器的输出是一个或者多个片段颜色,传递到管线的逐片段操作部分。 + +内建变量: gl_FragCoord:片段着色器中的一个只读变量。这个变量保存片段的窗口相对坐标。 ### GLSL的特点 @@ -81,9 +107,9 @@ GLSL中的数据类型主要分为标量、向量、矩阵、采样器、结构 ### 变量修饰符 - none:(默认的可省略)本地变量,可读可写,函数的输入参数既是这种类型 -- const:声明变量或函数的参数为只读类型 -- attribute:用于保存顶点或法线数据,它可以在数据缓冲区中读取数据,仅能用于顶点着色器 -- uniform:在运行时 shader 无法改变 uniform 变量,一般用来放置程序传递给 shader 的变换矩阵,材质,光照参数等等,可用于顶点着色器和片元着色器 +- const:常量 +- attribute:一般用于各个顶点各不相同的量。如顶点颜色、坐标等。用于保存顶点或法线数据,它可以在数据缓冲区中读取数据,仅能用于顶点着色器 +- uniform:统一变量。统一变量存储应用程序通过OpenGL ES API传入着色器的只读值。在运行时 shader 无法改变 uniform 变量,一般用来放置程序传递给 shader 的变换矩阵,材质,光照参数等等,可用于顶点着色器和片元着色器。如果统一变量在顶点着色器和片段着色器中均有声明,则声明的类型必须相同,且两个着色器中的值也需要相同。 - varying:易变量,用于修饰从顶点着色器向片元着色器传递的变量。一般是在光栅化图元的过程中计算生成,记录在每个片段中(而不是从顶点着色器直接传递给片元着色器) @@ -98,26 +124,17 @@ GLSL中的数据类型主要分为标量、向量、矩阵、采样器、结构 GLSL的类型转换与C不同。在GLSL中类型不可以自动提升,比如float a=1;就是一种错误的写法,必须严格的写成float a=1.0,也不可以强制转换,即float a=(float)1;也是错误的写法,但是可以用内置函数来进行转换,如float a=float(1);还有float a=float(true);(true为1.0,false为0.0)等,值得注意的是,低精度的int不能转换为低精度的float -### 限定符 - -与Java中的限定符类似,放在变量类型前面,并且只能用于全局变量。 - -- attritude:一般用于各个顶点各不相同的量。如顶点颜色、坐标等。 -- uniform:一般用于对于3D物体中所有顶点都相同的量。比如光源位置,统一变换矩阵等。 -- varying:表示易变量,一般用于顶点着色器传递到片元着色器的量。 -- const:常量。 - -- 流程控制 +### 流程控制 - 常用的与java基本一样 +常用的与java基本一样 -- 函数 +### 函数 - 定义函数的方式也与C语言基本相同。函数的返回值可以是GLSL中的除了采样器的任意类型。对于GLSL中函数的参数,可以用参数用途修饰符来进行修饰,常用修饰符如下: +定义函数的方式也与C语言基本相同。函数的返回值可以是GLSL中的除了采样器的任意类型。对于GLSL中函数的参数,可以用参数用途修饰符来进行修饰,常用修饰符如下: - - in:输入参数,无修饰符时默认为此修饰符。 - - out:输出参数。 - - inout:既可以作为输入参数,又可以作为输出参数。 +- in:输入参数,无修饰符时默认为此修饰符。 +- out:输出参数。 +- inout:既可以作为输入参数,又可以作为输出参数。 - 浮点精度 @@ -205,7 +222,7 @@ GLSL的类型转换与C不同。在GLSL中类型不可以自动提升,比如fl -[上一篇: 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) +[上一篇: 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 a8b903a6..84f3870c 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" @@ -7,12 +7,102 @@ GLES30作为我们与着色器连接的工具类提供了丰富的api。 上一篇文章说到GLSL中的变量修饰符有以下部分: - none:(默认的可省略)本地变量,可读可写,函数的输入参数既是这种类型 -- const:声明变量或函数的参数为只读类型 +- const:常量,声明变量或函数的参数为只读类型 - attribute:用于保存顶点或法线数据,它可以在数据缓冲区中读取数据,仅能用于顶点着色器 -- uniform:在运行时 shader 无法改变 uniform 变量,一般用来放置程序传递给 shader 的变换矩阵,材质,光照参数等等,可用于顶点着色器和片元着色器 +- uniform:统一变量在运行时 shader 无法改变 uniform 变量,一般用来放置程序传递给 shader 的变换矩阵,材质,光照参数等等,可用于顶点着色器和片元着色器 - varying:用于修饰从顶点着色器向片元着色器传递的变量 +了解一些常用API的意思能更方便后面的学习,这里就简单整理列了一些常用的部分: +- glClearColor(GLclampf red, GLclampf green, GLclampf blue, GLclampf alpha) + + 为颜色缓冲区指定清除值 + +- glClear(GLbitfileld mask) + + 清除预设值的缓冲区,参数分别为GL_COLOR_BUFFER_BIT(当前启用了颜色写入的缓冲区) GL_DEPTH_BUFFER_BIT(深度缓冲区) GL_STENCIL_BUFFER_BIT(模板缓冲区) + +- glActiveTexture(GLenum texture) + + 激活纹理单元。参数指定要激活的纹理单元,参数texture必须是GL_TEXTUREi之一,初始值为GL_TEXTURE0。 + +- glBindTexture(GLenum target, GLunit texture) + + 将一个特定的纹理ID绑定到一个纹理目标上 + +- glGenTextures(GLsizei n, GLunit * textures) + + 生成纹理ID,参数n是指定要生成的纹理ID的数量,textures是指定存储生成的纹理ID的数组 + +- glCreateShader(GLenum shaderType) + + 创建一个空的着色器对象,类型分别为GL_VERTEX_SHADER和GL_FRAGMENT_SHADER + +- glCompileShader(GLunit shader) + + 编译一个着色器对象。创建后要先编译才能运行。 + +- glAttachShader(GLunit program, GLunit shader) + + 将着色器对象附加到program对象 + +- glCreateProgram() + + 创建一个空的program对象并返回一个可以被引用的program id。 + +- glEnableVertexAttribArray(GLunit index)/glEnableVertexAttribArray(GLunit index) + + 启用或禁用通用定点属性数组。参数为指定要启用或禁用的通用顶点属性的索引。 + +- glDrawArrays(GLenum mode, GLinit first, GLsizei count) + + 参数mode为指定要渲染的图元类型,如GL_POINTS,GL_LINE_STRIP,GL_LINE_LOOP。first为指定已启用阵列中的起始索引。count为指定要渲染的索引数。在调用该方法前,可以使用glVertexAttribPointer预先指定单独的顶点、位置和颜色数组等信息。 + +- glDrawElements(GLenum mode, GLsizei count, GLenum type, const GLvoid * indices) + + mode是指定要渲染的图元类型,可以为GL_POINTS,GL_LINE_STRIP,GL_LINES等,count指定要渲染的元素数,type指定indices中的类型,必须是GL_UNSIGNED_BYTE或GL_UNSIGNED_SHORT。indices指定指向存储索引的位置的指针。使用该方法前也是需要使用glVertexAttribPointer来预先指定单独的顶点位置和颜色数据等信息。 + +- void glGetUniformfv(GLunit program, GLinit location, GLfloat *params) + + 通过params的形式返回指定统一变量的值,参数program指定要查询的program对象,location指定要查询的统一变量的位置,params返回指定的统一变量的值。 + +- void glGetVertexAttribfv(GLunit index, GLenum pname, GLFloat *params) + + 以params形式返回通用顶点属性参数的值。参数index是指定要查询的通用顶点的属性参数,pname是指定要查询的顶点属性参数的符号名称。可接受的值为GL_VERTEX_ATTRIB_ARRAY_BFFER_BINGDING等。 + +- void glGetVertexAttribPointerv(GLunit index, GLenum pname, GLvoid **pointer) + + 已**pointer返回指定的通用顶点属性的指针信息。参数index为要返回单额通用顶点属性采参数,pname是指定要返回的通用顶点属性参数的符号名称,必须是GL_VERTEX_ATTRIB_ARRAY_POINTER。 + +- glLinkProgram(GLunit program) + + 链接program指定的program对象,指定要链接的program对象的句柄。 + +- glTexImage2D(GLenum target, Glint level, GLinit internalformat, GLsizei width, GLsizei height, GLinit border, GLenum format, GLenum type, const GLvoid *data) + + 指定一个二维的纹理图片。纹理将指定纹理图像的一部分映射到纹理化为活动的每个图形基元。 + +- void glUniform1f(GLinit location, GLfloat v0) + +- void glUniformxx(GLinit l n,b njiu[uyi77u777uocation, GLfloat v0) + +- void glUniformMatrix1fx(GLinit location, GLsizei countM, GLboolean transpose, const GLfloat *valueM) + + 修改统一变量或统一变量数组的值。参数location为指定要修改的统一变量的位置,可以由glGetUniformLocation返回,v就是用于指定统一变量的新值。 + + count是指定要修改的元素数,value是指定指向将用于更新指定统一变量的count值数组的指针。countM指定要修改的矩阵数。 + +- glUseProgram(GLunit program) + + 使用程序对象program作为当前渲染状态的一部分。 + +- glVertexAttrib(GLunit index, GLFloat vo ...) + + 指定通用顶点属性的值 + +- glVertexAttribPointer(GLunit index, GLinit size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid *pointer) + + 指定索引index处的通用顶点属性数组的位置和数据格式,以便在渲染时使用。 ### 获取着色器程序内成员变量的id(句柄、指针) @@ -135,8 +225,7 @@ GLES20.glActiveTexture()确定了后续的纹理状态改变影响哪个纹理 - -[上一篇: 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) +[上一篇: 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 c712f597..ac99a6db 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" @@ -44,6 +44,26 @@ OpenGL不能直接加载jpg或者png这类被编码的压缩格式,需要加 当我们通过光栅化将图形处理成一个个小片段的时候,再讲纹理采样,渲染到指定位置上时,通常会遇到纹理元素和小片段并非一一对应。这时候,会出现纹理的压缩或者放大。那么在这两种情况下,就会有不同的处理方案,这就是纹理过滤了。 +### 纹理对象和纹理的加载 + +纹理应用的第一步是创建一个纹理对象。纹理对象是一个容器对象,保存渲染所需的纹理数据,例如图像数据、过滤模式和包装模式。在OpenGL ES中,纹理对象用一个无符号整数表示,该整数是纹理对象的一个句柄,用于生成纹理对象的函数是glGenTextures。 + +- glGenTextures(GLsizei n, GLunit *textures) + + 生成一个空的纹理对象,参数n是要生成的纹理对象的数量,textures是一个保存n个纹理对象ID的无符号整数数组。 + +- glBindTexture(GLenum target, GLunit texture) + + 将纹理对象绑定纹理目标,绑定目标后的下一个步骤是真正的加载图像数据。参数target是GL_TEXTURE_2D GL_TEXTURE_3D GL_TEXTURE_2D_ARRAY GL_TEXTURE_CUBE_MAP等目标。texture是要绑定的纹理对象句柄。 + +- glTexImage2D(GLenum target, GLinit level, GLenum internalFormat, GLsizei width, GLsizei height, GLinit boder, + + GLenum format, GLenum type, const void* pixels) + + 加载2D和立方图纹理图像数据。 + + + ### 加载纹理 下面是一个工具类方法,相对通用,能解决大部分需求,这个方法可以将内置的图片资源加载出对应的纹理ID。 @@ -186,11 +206,13 @@ public class TextureHelper { ``` #version 300 es precision mediump float; + // 采样器(sampler)是用于从纹理贴图读取的特殊统一变量。采样器统一变量将加载一个指定纹理绑定的纹理单元额数据,java代码里面需要把它设置为0 uniform sampler2D uTextureUnit; //接收刚才顶点着色器传入的纹理坐标(s,t) in vec2 vTexCoord; out vec4 vFragColor; void main() { + // texture函数会从纹理贴图中读取 vFragColor = texture(uTextureUnit,vTexCoord); } ``` @@ -336,7 +358,7 @@ public class TextureHelper { GLES30.glActiveTexture(GLES30.GL_TEXTURE0); //绑定纹理 GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureId); - // 将纹理单元传递片段着色器的u_TextureUnit + // 把着色里面的采样器统一变量sample设置为0 GLES20.glUniform1i(aTextureLocation, 0); @@ -528,7 +550,7 @@ public class TextureHelper { -[上一篇: 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) --- From 6521d4f088615938b2721ef473492cf6f18120d0 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 19 Mar 2020 23:25:48 +0800 Subject: [PATCH 008/213] add notes --- .../1.OpenGL\347\256\200\344\273\213.md" | 4 +- ...55\346\224\276\350\247\206\351\242\221.md" | 50 +++++++++- .../11.OpenGL ES\346\273\244\351\225\234.md" | 95 ++++++++++++++++++- ...66\344\270\211\350\247\222\345\275\242.md" | 21 ++-- .../9.OpenGL ES\347\272\271\347\220\206.md" | 15 +-- 5 files changed, 162 insertions(+), 23 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 3dc28da4..c611da9e 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,13 +1,13 @@ ## 1.OpenGL简介 +最近想要在播放器上做一个效果,需要用到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 ES 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 6c845636..e66638da 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" @@ -1,12 +1,58 @@ ## 10.GLSurfaceView+MediaPlayer播放视频 +### 相机预览 +1. 在GLSurfaceView.Render中创建一个纹理,再使用该纹理创建一个SurfaceTexture。 +2. 使用该SurfaceTexture创建一个Surface传给相机,相机预览数据就会输出到这个纹理上了。 +3. 使用GLSurfaceView.Render将该纹理渲染到GLSurfaceView的窗口上。 +4. 使用SurfaceTexture的setOnFrameAvailableListener方法给SurfaceTexture添加一个数据帧可用的监听器,在监听器中调用GLSurfaceView的requestRender方法渲染该帧数据,这样相机每次输出一帧数据就可以渲染一次,在GLSurfaceView窗口中就可以看到相机的预览数据了。 -平时的视频播放都是使用mediaplayer+textureview或者mediaplayer+surfaceview,但是如果我们要对视频进行一些OpenGL的操作,打个比方说我要在视频播放的时候添加一个滤镜,这个时候就需要用都SurfaceTexture了。 +- 顶点着色器 + + ```xml + #version 300 es + + layout (location = 0) in vec4 a_Position; + layout (location = 1) in vec2 a_texCoord; + + out vec2 v_texCoord; + + void main() + { + gl_Position = a_Position; + v_texCoord = a_texCoord; + } + ``` + +- 片段着色器 + + 这里需要注意一下,就是做相机预览的话,纹理的类型需要使用samplerExternalOES,而不是之前渲染图片的sampler2D。 + + ```xml + #version 300 es + #extension GL_OES_EGL_image_external_essl3 : require + precision mediump float; + + in vec2 v_texCoord; + out vec4 outColor; + uniform samplerExternalOES s_texture; + + void main(){ + outColor = texture(s_texture, v_texCoord); + } + ``` + + -### TextureView+MediaPlayer播放视频 + + + + +### 视频播放 + +平时的视频播放都是使用mediaplayer+textureview或者mediaplayer+surfaceview,但是如果我们要对视频进行一些OpenGL的操作,打个比方说我要在视频播放的时候添加一个滤镜,这个时候就需要用都SurfaceTexture了。 ```java public class VideoActivity extends Activity { 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 dc1cc40d..4110aa94 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" @@ -1,6 +1,46 @@ ## 11.OpenGL ES滤镜 -滤镜其实就是利用纹理。 +给图像添加滤镜本质就是图片处理,也就是对图片的像素进行计算,简单来说,图像处理的方法可以分为三类: + +- 点算:当前像素的处理只和自身的像素值有关,和其他像素无关,比如灰度处理。 +- 领域算:当前像素的处理需要和相邻的一定范围内的像素有关,比如高斯模糊。 +- 全局算:在全局上对所有像素进行统一变换,比如几何变换。 + +### 灰色滤镜 + +和前一篇文章基本一样,只是修改一下片段着色器的代码,原理就是在片段着色器中去处理颜色,让RGB三个通道的颜色取均值: + +``` +#version 300 es +#extension GL_OES_EGL_image_external_essl3 : require +precision mediump float; + +in vec2 v_texCoord; +out vec4 outColor; +uniform samplerExternalOES s_texture; + +//灰度滤镜,具体去处理颜色 +void grey(inout vec4 color){ + float weightMean = color.r * 0.3 + color.g * 0.59 + color.b * 0.11; + color.r = color.g = color.b = weightMean; +} + +void main(){ + //拿到颜色值 + vec4 tmpColor = texture(s_texture, v_texCoord); + //对颜色值进行处理 + grey(tmpColor); + //将处理后的颜色值输出到颜色缓冲区 + outColor = tmpColor; +} + +``` + + + + + + 滤镜的编写主要是靠基类。 @@ -16,14 +56,63 @@ RGB三个通道的颜色取反,而alpha通道不变。 -### 灰色滤镜 +```gl +//反向滤镜 +void reverse(inout vec4 color){ + color.r = 1.0 - color.r; + color.g = 1.0 - color.g; + color.b = 1.0 - color.b; +} + +``` + +### 黑白滤镜 + +```gals +//黑白滤镜 +void blackAndWhite(inout vec4 color){ + float threshold = 0.5; + float mean = (color.r + color.g + color.b) / 3.0; + color.r = color.g = color.b = mean >= threshold ? 1.0 : 0.0; +} +``` + -让RGB三个通道的颜色取均值 ### 位移滤镜 纹理默认传入的读取范围是(0,0)到(1,1)内的颜色值。如果对读取的位置进行调整修改,那么就可以做出各种各样的效果,例如缩放动画就是让读取的范围改成(-1,-1)到(2,2)。 +## 看完了疯了是不是,要做个滤镜效果,各种计算我实在弄不明白 + +最简单的方法就是通过LUT方法,通过设计师提供的LUT文件来实现预定的滤镜效果。基本思路如下: + +- 准备LUT文件 +- 加载LUT文件到OpenGL纹理 +- 将纹理传递给片段着色器 +- 根据LUT,在片段着色器中对图像的颜色值进行映射,得到滤镜后的颜色进行输出 + + + +### 离屏渲染 + +之前已经将相机的预览数据输出到OpenGL的纹理上,渲染的时候OpenGL直接将纹理渲染到屏幕上。但是如果想要对纹理进行进一步的处理,就不能直接渲染到屏幕上,而是需要先渲染到屏幕外的缓冲区(FrameBuffer)处理完后再渲染到屏幕。渲染到缓冲区的操作就是离屏渲染。主要步骤如下: + +- 准备离屏渲染所需要的FrameBuffer和纹理对象。 +- 切换渲染目标(屏幕->缓冲区) +- 执行渲染 +- 重置渲染目标(缓冲区 -> 屏幕) + + + + + + + + + + + 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 7b859d6d..335b56e0 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" @@ -113,9 +113,9 @@ Preferences -> Plugins -> 搜GLSL Support安装就可以了。 ```glsl // 声明着色器的版本 #version 300 es - // 顶点着色器的顶点位置,输入一个名为vPosition的4分量向量,layout (location = 0)表示这个变量的位置是顶点属性0。 + // 顶点着色器的顶点位置,输入一个名为vPosition的4分量向量,layout (location = 0)表示这个变量的位置是顶点属性中的第0个属性。 layout (location = 0) in vec4 vPosition; - // 顶点着色器的顶点颜色数据,输入一个名为aColor的4分量向量,layout (location = 1)表示这个变量的位置是顶点属性1。 + // 顶点着色器的顶点颜色数据,输入一个名为aColor的4分量向量,layout (location = 1)表示这个变量的位置是顶点属性中的第1个属性。 layout (location = 1) in vec4 aColor; // 输出一个名为vColor的4分量向量,后面输入到片段着色器中。 out vec4 vColor; @@ -129,6 +129,8 @@ Preferences -> Plugins -> 搜GLSL Support安装就可以了。 } ``` + 大体意思:使用OpenGL ES3.0版本,将图形顶点数据采用4分向量的数据结构绑定到着色器的第0个属性上,属性的名字是vPosition,然后再有一个颜色的4分向量绑定到做色漆的第1个属性上,属性的名字是aColor,另外还会输出一个vColor,着色器执行的时候,会将vPosition的值传递给用来表示顶点最终位置的内建变量gl_Position,将顶点最终大小的gl_PointSize设置为10,并将aColor的值复制给要输出额vColor。 + - 片段着色器(fragment_simple_shade.glsl) ```glsl @@ -211,12 +213,12 @@ class MyGLRenderer implements GLSurfaceView.Renderer { /*****************1.声明绘制图形的坐标和颜色数据 end**************/ public MyGLRenderer() { /****************2.为顶点位置及颜色申请本地内存 start************/ - //顶点位置相关 + //将顶点数据拷贝映射到native内存中,以便OpenGL能够访问 //分配本地内存空间,每个浮点型占4字节空间;将坐标数据转换为FloatBuffer,用以传入给OpenGL ES程序 - vertexBuffer = ByteBuffer.allocateDirect(triangleCoords.length * BYTES_PER_FLOAT) - .order(ByteOrder.nativeOrder()) - .asFloatBuffer(); - vertexBuffer.put(triangleCoords); + vertexBuffer = ByteBuffer.allocateDirect(triangleCoords.length * BYTES_PER_FLOAT) // 直接分配native内存 + .order(ByteOrder.nativeOrder()) // 和本地平台保持一致的字节序 + .asFloatBuffer(); // 将底层字节映射到FloatBuffer实例,方便使用 + vertexBuffer.put(triangleCoords); // 将顶点数据拷贝到native内存中 // 将数组数据put进buffer之后,指针并不是在首位,所以一定要position到0,至关重要!否则会有很多奇妙的错误!将缓冲区的指针移动到头部,保证数据是从最开始处读取 vertexBuffer.position(0); @@ -258,9 +260,10 @@ class MyGLRenderer implements GLSurfaceView.Renderer { //把颜色缓冲区设置为我们预设的颜色,绘图设计到多种缓冲区类型:颜色、深度和模板。这里只是向颜色缓冲区中绘制图形 GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT); - //绑定vertex坐标数据,告诉OpenGL可以在缓冲区vertextBuffer中获取数据 + //0是上面着色器中写的vPosition的变量位置(location = 0)。意思就是绑定vertex坐标数据,然后将在vertextBuffer中的顶点数据传给vPosition变量。 + // 你肯定会想,如果我在着色器中不写呢?int vposition = glGetAttribLocation(program, "vPosition");就可以获得他的属性位置了 GLES30.glVertexAttribPointer(0, 3, GLES30.GL_FLOAT, false, 0, vertexBuffer); - //启用顶点位置句柄 + //启用顶点变量,这个0也是vPosition在着色器变量中的位置,和上面一样,在着色器文件中的location=0声明的 GLES30.glEnableVertexAttribArray(0); //准备颜色数据 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 ac99a6db..25a09519 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" @@ -28,8 +28,6 @@ OpenGL中,2D纹理也有自己的坐标体系,取值范围在(0,0)到(1,1) 纹理上的每个顶点与定点坐标上的顶点一一对应。如下图,左边是顶点坐标,右边是纹理坐标,只要两个坐标的ABCD定义顺序一致,就可以正常地映射出对应的图形。顶点坐标内光栅化后的每个片段,都会在纹理坐标内取得对应的颜色值。 - - ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_es_texture_position.jpg) ### 纹理尺寸 @@ -38,7 +36,7 @@ OpenGL中,2D纹理也有自己的坐标体系,取值范围在(0,0)到(1,1) ### 文件读取 -OpenGL不能直接加载jpg或者png这类被编码的压缩格式,需要加载原始数据,也就是bitmpa。我们在内置图片到工程中,应该将图片放到drawable-nodpi中,避免读取时被压缩,通过BitmapFactory解码读取图片时,要设置为非缩放的方式,即options.isScaled=false。 +OpenGL不能直接加载jpg或者png这类被编码的压缩格式,需要加载原始数据,也就是bitmap。我们在内置图片到工程中,应该将图片放到drawable-nodpi中,避免读取时被压缩,通过BitmapFactory解码读取图片时,要设置为非缩放的方式,即options.isScaled=false。 ### 纹理过滤 @@ -212,7 +210,7 @@ public class TextureHelper { in vec2 vTexCoord; out vec4 vFragColor; void main() { - // texture函数会从纹理贴图中读取 + // texture函数会将传进来的纹理和坐标进行差值采样,输出到颜色缓冲区。 vFragColor = texture(uTextureUnit,vTexCoord); } ``` @@ -262,6 +260,7 @@ public class TextureHelper { * 坐标占用的向量个数 */ private static final int POSITION_COMPONENT_COUNT = 2; + // 逆时针顺序排列 private static final float[] POINT_DATA = { -0.5f, -0.5f, 0.5f, -0.5f, @@ -272,6 +271,7 @@ public class TextureHelper { * 颜色占用的向量个数 */ private static final int TEXTURE_COMPONENT_COUNT = 2; + // 纹理坐标(s, t),t坐标方向和顶点y坐标反着 private static final float[] TEXTURE_DATA = { 0.0f, 1.0f, 1.0f, 1.0f, @@ -313,10 +313,11 @@ public class TextureHelper { //在OpenGLES环境中使用程序 GLES30.glUseProgram(mProgram); - + // 获取这三个属性在着色器中的属性位置 uMatrixLocation = GLES30.glGetUniformLocation(mProgram, "u_Matrix"); aPositionLocation = GLES30.glGetAttribLocation(mProgram, "vPosition"); aTextureLocation = GLES30.glGetAttribLocation(mProgram, "aTextureCoord"); + // 将图片加载进来生成位图 textureId = TextureHelper.loadTexture(MyApplication.getInstance(), R.drawable.img).getTextureId(); } @@ -344,12 +345,12 @@ public class TextureHelper { //将变换矩阵传入顶点渲染器 GLES20.glUniformMatrix4fv(uMatrixLocation, 1, false, mProjectionMatrix, 0); - //准备坐标数据 + //传入坐标数据 GLES30.glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GLES30.GL_FLOAT, false, 0, vertexBuffer); //启用顶点位置句柄 GLES30.glEnableVertexAttribArray(aPositionLocation); - //准备颜色数据 + //传入颜色数据 GLES30.glVertexAttribPointer(aTextureLocation, TEXTURE_COMPONENT_COUNT, GLES30.GL_FLOAT, false, 0, textureBuffer); //启用顶点颜色句柄 GLES30.glEnableVertexAttribArray(aTextureLocation); From 429d21fe20670ebd1c124c8cc92c20d021b3706f Mon Sep 17 00:00:00 2001 From: CharonChui Date: Sat, 21 Mar 2020 20:13:09 +0800 Subject: [PATCH 009/213] update opengl notes --- .../1.OpenGL\347\256\200\344\273\213.md" | 8 ++--- ...55\346\224\276\350\247\206\351\242\221.md" | 4 ++- ...66\344\270\211\350\247\222\345\275\242.md" | 8 +++-- ...42\345\217\212\345\234\206\345\275\242.md" | 9 +++--- ...45\231\250\350\257\255\350\250\200GLSL.md" | 26 +++++++++++++---- ...\261\273\345\217\212Matrix\347\261\273.md" | 8 ++--- .../9.OpenGL ES\347\272\271\347\220\206.md" | 29 ++++++++++--------- ...72\347\241\200\347\237\245\350\257\206.md" | 2 +- 8 files changed, 56 insertions(+), 38 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 bbd77b44..3035317b 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" @@ -6,7 +6,7 @@ OpenGL(Open Graphics Library开放图形库)是用于渲染2D、3D矢量图形 OpenGL的前身是硅谷图形功能(SGI)公司为其图形工作站开发的IRIS GL,后来因为IRIS GL的移植性不好,所以在其基础上,开发出了OpenGl。OpenGl一般用于在图形工作站,PC端使用,由于性能各方面原因,在移动端使用OpenGl基本带不动。为此,Khronos公司就为OpenGl提供了一个子集,OpenGl ES(OpenGl for Embedded System)。 -网上已经有很多文章,这里为什么还要写?主要是因为最近在学的时候发现那些文章看完后还是雨里雾里的不明白。我想写一个简单的,能从入门开始一步步学习的,能简单的学会,文章里面有一些是从下面写的参考链接中拷贝过来的,也有一些是自己从书上看的。 +网上已经有很多文章,这里为什么还要写?主要是因为最近在学的时候发现那些文章看完后还是雨里雾里的不明白。方法不明白,不知道为什么要那样用,而且版本不同,最开始看的3.0版本,后面我就都用3.0的来写,结果网上的例子都是2.0的,GLSL的语法不一样,搞的死活出不来效果,耽误时间,所以干脆我系统学一遍,顺便记录下来,写一个简单的,能从入门开始一步步学习的,文章里面有一些是从下面写的参考链接中拷贝过来的,也有一些是自己从书上看的。本书所有的例子都是用OpenGL ES3.0版本、GLSL ES 300版本来写。 ### OpenGL ES @@ -21,9 +21,7 @@ OpenGL ES是免费的跨平台的功能完善的2D/3D图形库接口的API,是Op ### OpenGL的作用 - - -手机上做图像处理用很多方法,但是目前为止最高效的方法就是有效的使用图形处理单元(GPU),图像的处理和渲染就是在将要渲染到窗口上的像素上做很多的浮点匀速,而GPU可以并行的做浮点运算,所以用GPU来分担CPU的部分,可以提高效率。 +在手机上有两大元件,一个是CPU一个是GPU。显示图形界面也有两种方式,一个是使用CPU渲染,一个是使用GPU渲染,但是目前为止最高效的方法就是有效的使用图形处理单元(GPU),图像的处理和渲染就是在将要渲染到窗口上的像素上做很多的浮点匀速,而GPU可以并行的做浮点运算,所以用GPU来分担CPU的部分,可以提高效率,可以说GPU渲染其实是一种硬件加速。 - 图片处理:比如图片色调转换、美颜等。 - 摄像头预览效果处理。比如美颜相机、恶搞相机等。 @@ -229,7 +227,7 @@ Android 框架中有如下两个基本类,用于通过 OpenGL ES API 来创建 - + [下一篇: 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 2af47100..419edb37 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" @@ -1,8 +1,10 @@ ## 10.GLSurfaceView+MediaPlayer播放视频 +平时的视频播放都是使用mediaplayer+textureview或者mediaplayer+surfaceview,但是如果我们要对视频进行一些OpenGL的操作,打个比方说我要在视频播放的时候添加一个滤镜,这个时候就需要用都SurfaceTexture了。 +大体步骤如下: -平时的视频播放都是使用mediaplayer+textureview或者mediaplayer+surfaceview,但是如果我们要对视频进行一些OpenGL的操作,打个比方说我要在视频播放的时候添加一个滤镜,这个时候就需要用都SurfaceTexture了。 +GLSurfaceView -> setRender -> onSurfaceCreated回调方法中构造一个SurfaceTexture对象,然后设置到Camera预览或者MediaPlayer中 -> SurfaceTexture中的回调方法onFrameAvailable来得知一帧的数据真好完成 -> requestRender通知Render来绘制数据 -> 在Render的回调方法onDrawFrame中调用SurfaceTexture的updateTexImage方法来获取一帧数据,然后开始使用GL进行绘制预览。 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 dc8c01dd..ccc88ef9 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" @@ -8,8 +8,10 @@ OpenGL ES的绘制需要有以下步骤: - 顶点着色器 + 着色器都是使用GLSL语言来写的而GLSL版本之间有差异。 + ```glsl - #version 330 core + #version 300 es layout (location = 0) in vec3 position; void main() @@ -18,7 +20,7 @@ OpenGL ES的绘制需要有以下步骤: } ``` - 每个着色器都起始于一个版本声明。OpenGL 3.3以及和更高版本中,GLSL版本号和OpenGL的版本是匹配的(比如说GLSL 420版本对应于OpenGL 4.2)。我们同样明确表示我们会使用核心模式。 + 每个着色器都起始于一个版本声明,这里声明的是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)你后面会看到为什么我们会需要这个位置值。 @@ -33,7 +35,7 @@ OpenGL ES的绘制需要有以下步骤: 片断着色器全是关于计算你的像素最后的颜色输出。颜色使用RGBA。 ```glsl - #version 330 core + #version 300 es out vec4 color; void main() { color = vec4(1.0f, 0.5f, 0.2f, 1.0f); 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 3c40dc74..48ac41e1 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" @@ -307,7 +307,8 @@ GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT); //将变换矩阵传入顶点渲染器 - GLES20.glUniformMatrix4fv(uMatrixLocation,1,false,mMVPMatrix,0); + GLES + 0.glUniformMatrix4fv(uMatrixLocation,1,false,mMVPMatrix,0); //准备坐标数据 GLES30.glVertexAttribPointer(aPositionLocation, 3, GLES30.GL_FLOAT, false, 0, vertexBuffer); //启用顶点位置句柄 @@ -423,7 +424,7 @@ public class CircularRender implements GLSurfaceView.Renderer { @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { //将背景设置为白色 - GLES20.glClearColor(1.0f,1.0f,1.0f,1.0f); + GLES30.glClearColor(1.0f,1.0f,1.0f,1.0f); //编译顶点着色程序 String vertexShaderStr = ResReadUtils.readResource(R.raw.vertex_simple_shade); @@ -478,7 +479,7 @@ public class CircularRender implements GLSurfaceView.Renderer { GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT); //将变换矩阵传入顶点渲染器 - GLES20.glUniformMatrix4fv(uMatrixLocation,1,false,mMVPMatrix,0); + GLES30.glUniformMatrix4fv(uMatrixLocation,1,false,mMVPMatrix,0); //准备坐标数据 GLES30.glVertexAttribPointer(aPositionLocation, 3, GLES30.GL_FLOAT, false, 0, vertexBuffer); //启用顶点位置句柄 @@ -500,7 +501,7 @@ public class CircularRender implements GLSurfaceView.Renderer { ``` -[上一篇: 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) [下一篇: 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 0dd05b65..c177999b 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" @@ -3,7 +3,7 @@ 顶点着色器: ``` -#version 330 core +#version 300 es layout (location = 0) in vec3 position; // position变量的属性位置值为0 out vec4 vertexColor; // 为片段着色器指定一个颜色输出 @@ -18,7 +18,7 @@ void main() 片段着色器: ``` -#version 330 core +#version 300 es in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同) out vec4 color; // 片段着色器输出的变量名可以任意命名,类型必须是vec4 @@ -31,6 +31,20 @@ void main() +这里需要重点讲一下上面的版本: OpenGL ES版本有自己的着色器语言,其中OpenGL ES的版本与GLSL的版本有对应的关联关心: + +- 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) +- texture()方法代替了之前的texture2D()方法 +- 300版本中布局限定符可以声明顶点着色器输入和片段着色器输出的问题,例如layout(location = 2) in vec3 values[4]; +- 舍弃了gl_FragColor的内置属性,变成我们需要自己使用out关键字定义的属性,这就是为什么你从网还是哪个找到的代码换成GLSL ES 300版本后报错的原因。 + + + ### GLSL的特点 OpenGLES的着色器语言GLSL是一种高级的图形化编程语言,其源自应用广泛的C语言。与传统的C语言不同的是,它提供了更加丰富的针对于图像处理的原生类型,诸如向量、矩阵之类。GLSL主要包含以下特性: @@ -102,9 +116,9 @@ GLSL的类型转换与C不同。在GLSL中类型不可以自动提升,比如fl 与Java中的限定符类似,放在变量类型前面,并且只能用于全局变量。 -- attritude:一般用于各个顶点各不相同的量。如顶点颜色、坐标等。 +- attritude:一般用于各个顶点各不相同的量。如顶点颜色、坐标等。(GLSL 200 es版本的使用方法,在GLSL 300 es中已经被in替代) - uniform:一般用于对于3D物体中所有顶点都相同的量。比如光源位置,统一变换矩阵等。 -- varying:表示易变量,一般用于顶点着色器传递到片元着色器的量。 +- varying:表示易变量,一般用于顶点着色器传递到片元着色器的量。(GLSL 200 es版本的使用方法,在GLSL 300 es中已经被out替代) - const:常量。 - 流程控制 @@ -148,8 +162,8 @@ GLSL的类型转换与C不同。在GLSL中类型不可以自动提升,比如fl - gl_FragFacing:bool型,表示是否为属于光栅化生成此片元的对应图元的正面。 - gl_PointCoord:经过插值计算后的纹理坐标,点的范围是0.0到1.0 - 输出变量 - - gl_FragColor:当前片元颜色 - - gl_FragData:vec4类型的数据。设置当前片元的颜色,供渲染管线的后继过程使用。 + - gl_FragColor:当前片元颜色(GLSL 200 es版本的内置属性,在GLSL 300 es中已经没有了,需要自己用out关键字定义) + - gl_FragData:vec4类型的数据。设置当前片元的颜色,供渲染管线的后继过程使用。(GLSL 200 es版本的内置属性,在GLSL 300 es中已经没有了,需要自己用out关键字定义) ### 内置函数 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 a8b903a6..1c6bb981 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" @@ -2,15 +2,15 @@ -GLES30作为我们与着色器连接的工具类提供了丰富的api。 +GLES作为我们与着色器连接的工具类提供了丰富的api。 上一篇文章说到GLSL中的变量修饰符有以下部分: - none:(默认的可省略)本地变量,可读可写,函数的输入参数既是这种类型 - const:声明变量或函数的参数为只读类型 -- attribute:用于保存顶点或法线数据,它可以在数据缓冲区中读取数据,仅能用于顶点着色器 +- attribute:用于保存顶点或法线数据,它可以在数据缓冲区中读取数据,仅能用于顶点着色器(GLSL 200 es版本,在GLSL 300 es中已经被in替代) - uniform:在运行时 shader 无法改变 uniform 变量,一般用来放置程序传递给 shader 的变换矩阵,材质,光照参数等等,可用于顶点着色器和片元着色器 -- varying:用于修饰从顶点着色器向片元着色器传递的变量 +- varying:用于修饰从顶点着色器向片元着色器传递的变量(仅能用GLSL 200 es版本,在GLSL 300 es中已经被out替代) @@ -52,7 +52,7 @@ GLES20.glVertexAttribPointer(maTextureHandle, 2, GLES20.GL_FLOAT, false, 20, mRe ### 启用或禁用顶点属性数组 -调用GLES20.glEnableVertexAttribArray和GLES20.glDisableVertexAttribArray传入参数index。如果启用,那么当GLES20.glDrawArrays或者GLES20.glDrawElements被调用时,顶点属性数组会被使用。 +调用GLES30.glEnableVertexAttribArray和GLES30.glDisableVertexAttribArray传入参数index。如果启用,那么当GLES30.glDrawArrays或者GLES30.glDrawElements被调用时,顶点属性数组会被使用。 ### 选择活动纹理单元 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 c712f597..83480210 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,11 +4,11 @@ ### 纹理 -在OpenGL中简单理解就是一张图片。 +在OpenGL中简单理解就是一张图片,在学习之前需要明白这几个概念,不然很容易迷糊,不知道为什么要这样去调用api,到底是什么意思. -- 纹理Id:纹理的直接饮用 +- 纹理Id:句柄,纹理的直接引用 -- 纹理单元:纹理的操作容器,有GL_TEXTURE0、GL_TEXTURE1、GL_TEXTURE2等,纹理单元的数量是有限的,最多16个。所以在最多只能同时操作16个纹理。在切换使用纹理单元的时候,使用glActiveTexture方法。 +- 纹理单元:纹理的操作容器,有GL_TEXTURE0、GL_TEXTURE1、GL_TEXTURE2等,纹理单元的数量是有限的,最多16个。所以在最多只能同时操作16个纹理。在切换使用纹理单元的时候,使用glActiveTexture方法。也就是说OpenGL ES中内置了很多个纹理单元,并且是连续的,我们在使用的时候要选择其中一个,一般默认选择第一个(GLES_TEXTURE0),并且如果不选的话OpenGL默认激活的也就是第一个纹理单元。激活纹理单元后需要把它和纹理Id绑定,然后再通过GLES30.glUniform1i()方法传递给着色器中。 - 纹理目标:一个纹理单元中包含了多个类型的纹理目标,有GL_TEXTURE_1D、GL_TEXTURE_2D、CUBE_MAP等。本章中,将纹理ID绑定到纹理单元0的GL_TEXTURE_2D纹理目标上,之后对纹理目标的操作都是对纹理Id对应的数据进行操作。 @@ -83,15 +83,15 @@ public class TextureHelper { Log.w(TAG, "Resource ID " + resourceId + " could not be decoded."); } // 加载Bitmap资源失败,删除纹理Id - GLES20.glDeleteTextures(1, textureObjectIds, 0); + GLES30.glDeleteTextures(1, textureObjectIds, 0); return bean; } // 2. 将纹理绑定到OpenGL对象上 - GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureObjectIds[0]); + GLES30.glBindTexture(GLES20.GL_TEXTURE_2D, textureObjectIds[0]); // 3. 设置纹理过滤参数:解决纹理缩放过程中的锯齿问题。若不设置,则会导致纹理为黑色 - GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR_MIPMAP_LINEAR); - GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + GLES30.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR_MIPMAP_LINEAR); + GLES30.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); // 4. 通过OpenGL对象读取Bitmap数据,并且绑定到纹理对象上,之后就可以回收Bitmap对象 GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0); @@ -191,6 +191,7 @@ public class TextureHelper { in vec2 vTexCoord; out vec4 vFragColor; void main() { + // 100 es版本中是texture2D vFragColor = texture(uTextureUnit,vTexCoord); } ``` @@ -278,7 +279,7 @@ public class TextureHelper { @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { //将背景设置为白色 - GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f); + GLES30.glClearColor(1.0f, 1.0f, 1.0f, 1.0f); //编译顶点着色程序 String vertexShaderStr = ResReadUtils.readResource(R.raw.texture_vertex_simple_shade); @@ -321,7 +322,7 @@ public class TextureHelper { GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT); //将变换矩阵传入顶点渲染器 - GLES20.glUniformMatrix4fv(uMatrixLocation, 1, false, mProjectionMatrix, 0); + GLES30.glUniformMatrix4fv(uMatrixLocation, 1, false, mProjectionMatrix, 0); //准备坐标数据 GLES30.glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GLES30.GL_FLOAT, false, 0, vertexBuffer); //启用顶点位置句柄 @@ -341,7 +342,7 @@ public class TextureHelper { // 开始绘制 - GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, POINT_DATA.length / POSITION_COMPONENT_COUNT); + GLES30.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, POINT_DATA.length / POSITION_COMPONENT_COUNT); //禁止顶点数组的句柄 GLES30.glDisableVertexAttribArray(aPositionLocation); GLES30.glDisableVertexAttribArray(aTextureLocation); @@ -437,7 +438,7 @@ public class TextureHelper { @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { //将背景设置为白色 - GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f); + GLES30.glClearColor(1.0f, 1.0f, 1.0f, 1.0f); //编译顶点着色程序 String vertexShaderStr = ResReadUtils.readResource(R.raw.texture_vertex_simple_shade); @@ -480,7 +481,7 @@ public class TextureHelper { GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT); //将变换矩阵传入顶点渲染器 - GLES20.glUniformMatrix4fv(uMatrixLocation, 1, false, mProjectionMatrix, 0); + GLES30.glUniformMatrix4fv(uMatrixLocation, 1, false, mProjectionMatrix, 0); //准备坐标数据 GLES30.glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GLES30.GL_FLOAT, false, 0, vertexBuffer); @@ -498,9 +499,9 @@ public class TextureHelper { //绑定纹理 GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureId); // 将纹理单元传递片段着色器的u_TextureUnit - GLES20.glUniform1i(aTextureLocation, 0); + GLES30.glUniform1i(aTextureLocation, 0); // 开始绘制 - GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, POINT_DATA.length / POSITION_COMPONENT_COUNT); + GLES30.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, POINT_DATA.length / POSITION_COMPONENT_COUNT); //准备第二个纹理的坐标数据 diff --git "a/VideoDevelopment/\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" "b/VideoDevelopment/\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" index 398deaa6..a3329194 100644 --- "a/VideoDevelopment/\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" +++ "b/VideoDevelopment/\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" @@ -168,7 +168,7 @@ YUV --- -YUV(也成YCbCr)是电视系统所采用的一种颜色编码方法。其中Y表示明亮度也就是灰阶值,它是基础信号。U和V表示的则是色度,UV的作用是描述影像色彩及饱和度,它们用于指定像素的颜色。U和V不是基础信号,他俩都是被正交调制的。 +YUV(也成YCbCr)是电视系统所采用的一种颜色编码方法,他是一种亮度与色度分离的色彩格式。其中Y表示明亮度也就是灰阶值,它是基础信号。U和V表示的则是色度,UV的作用是描述影像色彩及饱和度,它们用于指定像素的颜色。U和V不是基础信号,他俩都是被正交调制的。早期的电视都是黑白的,即只有亮度值(Y),有了彩色电视之后,加入了UV两种色度,形成现在的YUV,也叫YCbCr。人眼对亮度敏感,对色度不敏感,因此减少部分UV的数据量,人眼也无法感知出来,这样就可以通过压缩UV的分辨率,在不影响观感的前提下,减少视频的体积。 YUV和RGB视频信号相比,最大的优点在于只需要占用极少的带宽,YUV只需要占用RGB一般的带宽。 From a4e7ce71c81917e313c4d28860a8215ff1aafe9e Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 23 Mar 2020 21:39:10 +0800 Subject: [PATCH 010/213] update notes --- .../1.OpenGL\347\256\200\344\273\213.md" | 6 +- ...55\346\224\276\350\247\206\351\242\221.md" | 132 ++-- ...66\344\270\211\350\247\222\345\275\242.md" | 129 ++-- ...42\345\217\212\345\234\206\345\275\242.md" | 566 ++++++++---------- ...45\231\250\350\257\255\350\250\200GLSL.md" | 50 +- ...\261\273\345\217\212Matrix\347\261\273.md" | 17 +- .../9.OpenGL ES\347\272\271\347\220\206.md" | 404 +++++-------- 7 files changed, 609 insertions(+), 695 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 12fb97af..30041c0d 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" @@ -11,6 +11,10 @@ OpenGL的前身是硅谷图形功能(SGI)公司为其图形工作站开发的IRI 网上已经有很多文章,这里为什么还要写?主要是因为最近在学的时候发现那些文章看完后还是雨里雾里的不明白。方法不明白,不知道为什么要那样用,而且版本不同,最开始看的3.0版本,后面我就都用3.0的来写,结果网上的例子都是2.0的,GLSL的语法不一样,搞的死活出不来效果,耽误时间,所以干脆我系统学一遍,顺便记录下来,写一个简单的,能从入门开始一步步学习的,文章里面有一些是从下面写的参考链接中拷贝过来的,也有一些是自己从书上看的。本书所有的例子都是用OpenGL ES3.0版本、GLSL ES 300版本来写。 + +[后面文章所有的源码都在Github上](https://github.com/CharonChui/OpenGLES3.0StudyDemo) + + ### OpenGL ES [OpenGL ES 官网](https://www.khronos.org/opengles/) @@ -140,7 +144,7 @@ OpenGL ES 3.0实现了具有可编程着色功能的图形管线,有两个规 - 坐标系 - OpenGL ES采用虚拟坐标系 + OpenGL ES是一个3D的世界,由x、y、z坐标组成顶点坐标。采用虚拟的右手坐标。 ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_es_xy.png) 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 b27b0da2..a9ab7d17 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" @@ -1,6 +1,14 @@ ## 10.GLSurfaceView+MediaPlayer播放视频 -### 相机预览 +### 视频播放 + +平时的视频播放都是使用mediaplayer+textureview或者mediaplayer+surfaceview,但是如果我们要对视频进行一些OpenGL的操作,打个比方说我要在视频播放的时候添加一个滤镜,这个时候就需要用都SurfaceTexture了。 + +大体步骤如下: + +GLSurfaceView -> setRender -> onSurfaceCreated回调方法中构造一个SurfaceTexture对象,然后设置到Camera预览或者MediaPlayer中 -> SurfaceTexture中的回调方法onFrameAvailable来得知一帧的数据真好完成 -> requestRender通知Render来绘制数据 -> 在Render的回调方法onDrawFrame中调用SurfaceTexture的updateTexImage方法来获取一帧数据,然后开始使用GL进行绘制预览。 + +​ 具体步骤: 1. 在GLSurfaceView.Render中创建一个纹理,再使用该纹理创建一个SurfaceTexture。 2. 使用该SurfaceTexture创建一个Surface传给相机,相机预览数据就会输出到这个纹理上了。 @@ -11,116 +19,84 @@ ```xml #version 300 es + in vec4 vPosition; + in vec2 vCoordPosition; + out vec2 aCoordPosition; - layout (location = 0) in vec4 a_Position; - layout (location = 1) in vec2 a_texCoord; - - out vec2 v_texCoord; - - void main() - { - gl_Position = a_Position; - v_texCoord = a_texCoord; + void main() { + gl_Position = vPosition; + aCoordPosition = vCoordPosition; } ``` - + - 片段着色器 - 这里需要注意一下,就是做相机预览的话,纹理的类型需要使用samplerExternalOES,而不是之前渲染图片的sampler2D。 + 这里需要注意一下,就是做相机预览和视频播放的话,纹理的类型需要使用samplerExternalOES,而不是之前渲染图片的sampler2D。这是因为相机和视频的数据是YUV的,而OpenGL ES是RGB的,samplerExternalOES内部会进行处理。#extension用于启用和设置扩展的行为。格式为#extension all : behavior。behavior的可选值有: require、enable、warn、disable。 ```xml #version 300 es #extension GL_OES_EGL_image_external_essl3 : require precision mediump float; - - in vec2 v_texCoord; - out vec4 outColor; - uniform samplerExternalOES s_texture; - - void main(){ - outColor = texture(s_texture, v_texCoord); + in vec2 aCoordPosition; + uniform samplerExternalOES uSamplerTexture; + out vec4 vFragColor; + void main() { + vFragColor = texture(uSamplerTexture, aCoordPosition); } ``` - - - - - - -### 视频播放 - -平时的视频播放都是使用mediaplayer+textureview或者mediaplayer+surfaceview,但是如果我们要对视频进行一些OpenGL的操作,打个比方说我要在视频播放的时候添加一个滤镜,这个时候就需要用都SurfaceTexture了。 - -大体步骤如下: - -GLSurfaceView -> setRender -> onSurfaceCreated回调方法中构造一个SurfaceTexture对象,然后设置到Camera预览或者MediaPlayer中 -> SurfaceTexture中的回调方法onFrameAvailable来得知一帧的数据真好完成 -> requestRender通知Render来绘制数据 -> 在Render的回调方法onDrawFrame中调用SurfaceTexture的updateTexImage方法来获取一帧数据,然后开始使用GL进行绘制预览。 - - - -### TextureView+MediaPlayer播放视频 +### GLSurfaceView+MediaPlayer播放视频 ```java -public class VideoActivity extends Activity { - private static final String VIDEO_PATH = "http://60.28.125.129/video19.ifeng.com/video06/2012/04/11/629da9ec-60d4-4814-a940-997e6487804a.mp4"; +public class VideoPlayerActivity extends Activity { + private GLSurfaceView mGLSurfaceView; private Surface mSurface; - private TextureView mTextureView; private MediaPlayer mMediaPlayer; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.activity_video); - mTextureView = findViewById(R.id.textureview); - mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { + mGLSurfaceView = new GLSurfaceView(this); + Display display = getWindowManager().getDefaultDisplay(); + int width = display.getWidth(); + int height = (int) ((width) * (9 / 16f)); + ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(width, height); + mGLSurfaceView.setLayoutParams(layoutParams); + setContentView(mGLSurfaceView); + mGLSurfaceView.setEGLContextClientVersion(3); + VideoPlayerRender videoPlayerRender = new VideoPlayerRender(mGLSurfaceView); + videoPlayerRender.setIVideoTextureRenderListener(new VideoPlayerRender.IVideoTextureRenderListener() { @Override - public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { - // 创建surface对象,让其从surfacetexture中获取数据 - mSurface = new Surface(surface); + public void onCreate(SurfaceTexture surfaceTexture) { + mSurface = new Surface(surfaceTexture); startPlay(); } - - @Override - public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { - - } - - @Override - public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { - mSurface = null; - if (mMediaPlayer != null) { - mMediaPlayer.stop(); - mMediaPlayer.release(); - mMediaPlayer = null; - } - return true; - } - - @Override - public void onSurfaceTextureUpdated(SurfaceTexture surface) { - - } }); + mGLSurfaceView.setRenderer(videoPlayerRender); } @Override protected void onResume() { super.onResume(); - if (mTextureView.isAvailable()) { + if (mSurface != null && mSurface.isValid()) { startPlay(); } + if (mGLSurfaceView != null) { + mGLSurfaceView.onResume(); + } } private void startPlay() { - if (mMediaPlayer != null) { + if (mMediaPlayer != null && mSurface != null && mSurface.isValid()) { + mMediaPlayer.setSurface(mSurface); + mMediaPlayer.start(); return; } mMediaPlayer = new MediaPlayer(); try { - mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); - mMediaPlayer.setDataSource(this, Uri.parse(VIDEO_PATH)); + mMediaPlayer = MediaPlayer.create(this, R.raw.beauty); mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mp) { @@ -137,6 +113,7 @@ public class VideoActivity extends Activity { }); // 将surface设置给mediaplayer mMediaPlayer.setSurface(mSurface); + mSurface.release(); mMediaPlayer.setScreenOnWhilePlaying(true); mMediaPlayer.setLooping(true); mMediaPlayer.prepareAsync(); @@ -148,6 +125,17 @@ public class VideoActivity extends Activity { @Override protected void onPause() { super.onPause(); + if (mGLSurfaceView != null) { + mGLSurfaceView.onPause(); + } + if (mMediaPlayer != null && mMediaPlayer.isPlaying()) { + mMediaPlayer.pause(); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); if (mMediaPlayer != null) { mMediaPlayer.reset(); mMediaPlayer.release(); @@ -157,9 +145,9 @@ public class VideoActivity extends Activity { } ``` +视频是能播起来了,但是变形了 - -### 增加OpenGL ES +### 修复变形 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 09b102df..624f5d0a 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" @@ -110,7 +110,7 @@ Preferences -> Plugins -> 搜GLSL Support安装就可以了。 安装完GLSL插件后,就可以开始了。一般将GLSL文件放到raw或assets目录,我们这里在raw目录上右键New,然后选择GLSL Shader创建就可以了,创建后默认会生成一个main()函数。 -- 顶点着色器(vertex_simple_shade.glsl) +- 顶点着色器(triangle_vertex_shader.glsl) ```glsl // 声明着色器的版本 @@ -133,7 +133,7 @@ Preferences -> Plugins -> 搜GLSL Support安装就可以了。 大体意思:使用OpenGL ES3.0版本,将图形顶点数据采用4分向量的数据结构绑定到着色器的第0个属性上,属性的名字是vPosition,然后再有一个颜色的4分向量绑定到做色漆的第1个属性上,属性的名字是aColor,另外还会输出一个vColor,着色器执行的时候,会将vPosition的值传递给用来表示顶点最终位置的内建变量gl_Position,将顶点最终大小的gl_PointSize设置为10,并将aColor的值复制给要输出额vColor。 -- 片段着色器(fragment_simple_shade.glsl) +- 片段着色器(triangle_fragment_shader.glsl) ```glsl // 声明着色器的版本 @@ -164,29 +164,40 @@ Preferences -> Plugins -> 搜GLSL Support安装就可以了。 6. 完成绘制 ```java -public class MainActivity extends AppCompatActivity { +public class TriangleActivity extends Activity { private GLSurfaceView mGlSurfaceView; + private TriangleRender mTriangleRender; @Override - protected void onCreate(Bundle savedInstanceState) { + protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - mGlSurfaceView = new MyGLSurfaceView(this); - setContentView(mGlSurfaceView); + setContentView(R.layout.activity_triangle); + mGlSurfaceView = findViewById(R.id.mGLSurfaceView); + // OpenGL ES 3.0版本 + mGlSurfaceView.setEGLContextClientVersion(3); + mTriangleRender = new TriangleRender(); + mGlSurfaceView.setRenderer(mTriangleRender); + mGlSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); } -} -class MyGLSurfaceView extends GLSurfaceView { - private final MyGLRenderer renderer; + @Override + protected void onPause() { + super.onPause(); + mGlSurfaceView.onPause(); + } - public MyGLSurfaceView(Context context) { - super(context); - setEGLContextClientVersion(3); - renderer = new MyGLRenderer(); - setRenderer(renderer); + @Override + protected void onResume() { + super.onResume(); + mGlSurfaceView.onResume(); } } +``` + +TriangleRender的实现如下: -class MyGLRenderer implements GLSurfaceView.Renderer { +```java +public class TriangleRender implements GLSurfaceView.Renderer { //一个Float占用4Byte private static final int BYTES_PER_FLOAT = 4; //三个顶点 @@ -212,8 +223,9 @@ class MyGLRenderer implements GLSurfaceView.Renderer { 0.0f, 1.0f, 0.0f, 1.0f,// bottom left 0.0f, 0.0f, 1.0f, 1.0f// bottom right }; + /*****************1.声明绘制图形的坐标和颜色数据 end**************/ - public MyGLRenderer() { + public TriangleRender() { /****************2.为顶点位置及颜色申请本地内存 start************/ //将顶点数据拷贝映射到native内存中,以便OpenGL能够访问 //分配本地内存空间,每个浮点型占4字节空间;将坐标数据转换为FloatBuffer,用以传入给OpenGL ES程序 @@ -240,10 +252,10 @@ class MyGLRenderer implements GLSurfaceView.Renderer { /******************3.加载编译顶点着色器和片段着色器 start**********/ //编译顶点着色程序 - String vertexShaderStr = readResource(MyApplication.getInstance(), R.raw.vertex_simple_shade); + String vertexShaderStr = readResource(MyApplication.getInstance(), R.raw.triangle_vertex_shader); int vertexShaderId = compileVertexShader(vertexShaderStr); //编译片段着色程序 - String fragmentShaderStr = readResource(MyApplication.getInstance(), R.raw.fragment_simple_shade); + String fragmentShaderStr = readResource(MyApplication.getInstance(), R.raw.triangle_fragment_shader); int fragmentShaderId = compileFragmentShader(fragmentShaderStr); /******************3.加载编译顶点着色器和片段着色器 end**********/ /******************4.创建program,连接顶点和片段着色器并链接program start***********/ @@ -253,24 +265,27 @@ class MyGLRenderer implements GLSurfaceView.Renderer { //在OpenGLES环境中使用程序 GLES30.glUseProgram(mProgram); } + public void onSurfaceChanged(GL10 unused, int width, int height) { /*********5.设置绘制窗口********/ GLES30.glViewport(0, 0, width, height); } + public void onDrawFrame(GL10 unused) { /**********6.绘制************/ //把颜色缓冲区设置为我们预设的颜色,绘图设计到多种缓冲区类型:颜色、深度和模板。这里只是向颜色缓冲区中绘制图形 GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT); - + // 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, 0, vertexBuffer); //启用顶点变量,这个0也是vPosition在着色器变量中的位置,和上面一样,在着色器文件中的location=0声明的 GLES30.glEnableVertexAttribArray(0); //准备颜色数据 /** - * glVertexAttribPointer()方法的参数上面的也说过了,这里再按照这个场景说一下分别为: + * 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); @@ -278,6 +293,7 @@ class MyGLRenderer implements GLSurfaceView.Renderer { * stride:跨距,默认是0。(由于我们将顶点位置和颜色数据分别存放没写在一个数组中,所以使用默认值0) * ptr: 本地数据缓存(这里我们的是顶点的位置和颜色数据)。 */ + // 1是aColor在属性的位置,4是因为我们声明的颜色是4位,r、g、b、a。 GLES30.glVertexAttribPointer(1, 4, GLES30.GL_FLOAT, false, 0, colorBuffer); //启用顶点颜色句柄 GLES30.glEnableVertexAttribArray(1); @@ -296,6 +312,7 @@ class MyGLRenderer implements GLSurfaceView.Renderer { GLES30.glDisableVertexAttribArray(0); GLES30.glDisableVertexAttribArray(1); } + /** * 编译顶点着色器 * @@ -417,6 +434,8 @@ class MyGLRenderer implements GLSurfaceView.Renderer { } ``` + + 效果如下: ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_es_tri.jpg) @@ -688,15 +707,14 @@ public class ShaderUtils { ```java public class ResReadUtils { - /** * 读取资源 * @param resourceId */ - public static String readResource(int resourceId) { + public static String readResource(@NonNull Context context, @RawRes int resourceId) { StringBuilder builder = new StringBuilder(); try { - InputStream inputStream = MyApplication.getInstance().getResources().openRawResource(resourceId); + InputStream inputStream = context.getResources().openRawResource(resourceId); InputStreamReader streamReader = new InputStreamReader(inputStream); BufferedReader bufferedReader = new BufferedReader(streamReader); @@ -717,19 +735,19 @@ public class ResReadUtils { -- 修改顶点着色器,增加矩阵变换(修改vertex_simple_shade.glsl) +- 修改顶点着色器,增加矩阵变换(修改iso_triangle_vertex_shader.glsl),片段着色器不用修改 ```glsl + // 声明着色器的版本,OpenGL ES 3.0版本对应的着色器语言版本是 GLSL 300 ES #version 300 es - // 输入一个名为vPosition的4分量向量,layout (location = 0)表示这个变量的位置是顶点属性0。 + // 顶点着色器的顶点位置,输入一个名为vPosition的4分量向量,layout (location = 0)表示这个变量的位置是顶点属性中的第0个属性。 layout (location = 0) in vec4 vPosition; - // 输入一个名为aColor的4分量向量,layout (location = 1)表示这个变量的位置是顶点属性1。 + // 顶点着色器的顶点颜色数据,输入一个名为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; @@ -744,10 +762,41 @@ public class ResReadUtils { - 把变换矩阵设置给顶点渲染器 -其他所有代码都和上面的一样,只是在Renderer的实现类中增加对转换矩阵的部分 +其他所有代码都和上面的一样,只是在Renderer的实现类中增加对转换矩阵的部分,这里我们使用GLTextureView来实现: + +```java +public class IsoTriangleActivity extends Activity { + private GLTextureView mGLTextureView; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_iso_triangle); + mGLTextureView = findViewById(R.id.mGLTextureView); + IsoTriangleRender render = new IsoTriangleRender(); + mGLTextureView.setEGLContextClientVersion(3); + mGLTextureView.setRenderer(render); + mGLTextureView.setRenderMode(GLTextureView.RENDERMODE_WHEN_DIRTY); + } + + @Override + protected void onPause() { + super.onPause(); + mGLTextureView.onPause(); + } + + @Override + protected void onResume() { + super.onResume(); + mGLTextureView.onResume(); + } +} +``` + +IsoTriangleRender的实现如下: ```java -class TriangleRender implements GLSurfaceView.Renderer { +public class IsoTriangleRender implements GLTextureView.Renderer { //一个Float占用4Byte private static final int BYTES_PER_FLOAT = 4; //三个顶点 @@ -780,7 +829,7 @@ class TriangleRender implements GLSurfaceView.Renderer { 0.0f, 0.0f, 1.0f, 1.0f// bottom right }; - public TriangleRender() { + public IsoTriangleRender() { //顶点位置相关 //分配本地内存空间,每个浮点型占4字节空间;将坐标数据转换为FloatBuffer,用以传入给OpenGL ES程序 vertexBuffer = ByteBuffer.allocateDirect(triangleCoords.length * BYTES_PER_FLOAT) @@ -802,13 +851,13 @@ class TriangleRender implements GLSurfaceView.Renderer { //将背景设置为白色 GLES30.glClearColor(1.0f, 1.0f, 1.0f, 1.0f); //编译顶点着色程序 - String vertexShaderStr = readResource(R.raw.vertex_simple_shade); - int vertexShaderId = compileVertexShader(vertexShaderStr); + String vertexShaderStr = ResReadUtils.readResource(MyApplication.getInstance(), R.raw.iso_triangle_vertex_shader); + int vertexShaderId = ShaderUtils.compileVertexShader(vertexShaderStr); //编译片段着色程序 - String fragmentShaderStr = readResource(R.raw.fragment_simple_shade); - int fragmentShaderId = compileFragmentShader(fragmentShaderStr); + String fragmentShaderStr = ResReadUtils.readResource(MyApplication.getInstance(), R.raw.iso_triangle_fragment_shader); + int fragmentShaderId = ShaderUtils.compileFragmentShader(fragmentShaderStr); //连接程序 - mProgram = linkProgram(vertexShaderId, fragmentShaderId); + mProgram = ShaderUtils.linkProgram(vertexShaderId, fragmentShaderId); //在OpenGLES环境中使用程序 GLES30.glUseProgram(mProgram); @@ -846,7 +895,7 @@ class TriangleRender implements GLSurfaceView.Renderer { } @Override - public void onDrawFrame(GL10 gl) { + public boolean onDrawFrame(GL10 gl) { //把颜色缓冲区设置为我们预设的颜色 GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT); //绑定vertex坐标数据,告诉OpenGL可以在缓冲区vertexBuffer中获取vPosition的护具 @@ -862,12 +911,20 @@ class TriangleRender implements GLSurfaceView.Renderer { //禁止顶点数组的句柄 GLES30.glDisableVertexAttribArray(0); GLES30.glDisableVertexAttribArray(1); + return true; + } + + @Override + public void onSurfaceDestroyed() { + } } ``` + + [上一篇: 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 8e079b88..a9ed80f0 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,5 +1,145 @@ ## 6.OpenGL ES绘制矩形及圆形 +写完上面的绘制三角形的部分,手快抽筋了,为啥? 一个就是每个方法都要调用GLES30.还有一个就是我们完全可以把公共的代码再封装到一个Base类里面啊,所以我这里就抽了一个BaseGLSurfaceViewRenderer类,然后把GLES30中一些常用的方法写了一遍。为什么要这样做呢? 我们也可以通过import static来省去GLES30的写法,但我不喜欢那样。另外还弄了BufferUtil、ProjectionMatrixUtil的类: + +```java +public class BufferUtil { + private static final int BYTES_PER_FLOAT = 4; + + public static FloatBuffer getFloatBuffer(float[] array) { + FloatBuffer floatBuffer = ByteBuffer.allocateDirect(array.length * BYTES_PER_FLOAT) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + floatBuffer.put(array); + floatBuffer.position(0); + return floatBuffer; + } +} +``` + +```java +public class ProjectionMatrixUtil { + // 矩阵数组 + private static final float[] mProjectionMatrix = new float[]{ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1, + }; + + public static void orthoM(int program, int width, int height, String name) { + int uMatrixLocation = GLES30.glGetUniformLocation(program, name); + //计算宽高比 边长比(>=1),非宽高比 + float aspectRatio = width > height ? + (float) width / (float) height : + (float) height / (float) width; + if (width > height) { + // 横屏 + Matrix.orthoM(mProjectionMatrix, 0, -aspectRatio, aspectRatio, -1f, 1f, -1f, 1f); + } else { + // 竖屏or正方形 + Matrix.orthoM(mProjectionMatrix, 0, -1f, 1f, -aspectRatio, aspectRatio, -1f, 1f); + } + GLES30.glUniformMatrix4fv(uMatrixLocation, 1, false, mProjectionMatrix, 0); + } +} +``` + + + +```java +public abstract class BaseGLSurfaceViewRenderer implements GLSurfaceView.Renderer { + protected int mProgram; + + /** + * readResource -> compileShader -> linkProgram -> useProgram + * + * @param context + * @param vertexShader + * @param fragmentShader + */ + protected void handleProgram(@NonNull Context context, @RawRes int vertexShader, @RawRes int fragmentShader) { + String vertexShaderStr = ResReadUtils.readResource(context, vertexShader); + int vertexShaderId = ShaderUtils.compileVertexShader(vertexShaderStr); + //编译片段着色程序 + String fragmentShaderStr = ResReadUtils.readResource(context, fragmentShader); + int fragmentShaderId = ShaderUtils.compileFragmentShader(fragmentShaderStr); + //连接程序 + mProgram = ShaderUtils.linkProgram(vertexShaderId, fragmentShaderId); + //在OpenGLES环境中使用程序 + GLES30.glUseProgram(mProgram); + } + + protected void glViewport(int x, int y, int width, int height) { + GLES30.glViewport(x, y, width, height); + } + + protected void glClearColor(float red, float green, float blue, float alpha) { + GLES30.glClearColor(red, green, blue, alpha); + } + + protected void glClear(int mask) { + GLES30.glClear(mask); + } + + protected static void glEnableVertexAttribArray(int index) { + GLES30.glEnableVertexAttribArray(index); + } + + protected void glDisableVertexAttribArray(int index) { + GLES30.glDisableVertexAttribArray(index); + } + + protected int glGetAttribLocation(String name) { + return GLES30.glGetAttribLocation(mProgram, name); + } + + protected int glGetUniformLocation(String name) { + return GLES30.glGetUniformLocation(mProgram, name); + } + + protected void glUniformMatrix4fv(int location, int count, boolean transpose, float[] value, int offset) { + GLES30.glUniformMatrix4fv(location, count, transpose, value, offset); + } + + protected void glDrawArrays(int mode, int first, int count) { + GLES30.glDrawArrays(mode, first, count); + } + + protected void glDrawElements(int mode, int count, int type, int offset) { + GLES30.glDrawElements(mode, count, type, offset); + } + + protected void orthoM(String name, int width, int height) { + ProjectionMatrixUtil.orthoM(mProgram, width, height, name); + } + + protected void glVertexAttribPointer( + int indx, + int size, + int type, + boolean normalized, + int stride, + java.nio.Buffer ptr) { + GLES30.glVertexAttribPointer(indx, size, type, normalized, stride, ptr); + } +} +``` + + + +前面绘制点、线、三角形的时候在用GLES30.glDrawArrays(GL_TRIANGLE_STRIP)方法时选择的mode是GL_TRIANGLE_STRIP,这个mode还有其他的类型,假设我现在想要绘制一个6边形呢,这里以A、B、C、D、E、F六个点来说一下mode的区别: + +- GL_POINTS : 绘制独立的点。 +- GL_LINES : 绘制每两个点的一条线。AB、CD、EF +- GL_LINE_LOOP : 按顺序将所有的点都连接起来,包括收尾相连。AB、BC、CD、DE、EF、FA +- GL_LINE_STRIP:按顺序将所有的点连接起来,不包括收尾相连。AB、BC、CD、DE、EF +- GL_TRIANGLES:每3个点构成一个三角形。 ABC、DEF +- GL_TRIANGLES_STRIP:相邻3个点构成一个三角形,不包括收尾两个点。ABC、BCD、CDE、DEF +- GL_TRIANGLE_FAN:第一个点和之后所有相邻的两个点构成一个三角形。ABC、ACD、ADE、AEF + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_es_gl_triangle.jpg) + ### 顶点法和索引法 @@ -29,38 +169,11 @@ 顶点法: ```java - package com.charon.opengldemo.rectangle; - - import android.opengl.GLES20; - import android.opengl.GLES30; - import android.opengl.GLSurfaceView; - import android.opengl.Matrix; - - import com.charon.opengldemo.R; - import com.charon.opengldemo.util.ResReadUtils; - import com.charon.opengldemo.util.ShaderUtils; - - import java.nio.ByteBuffer; - import java.nio.ByteOrder; - import java.nio.FloatBuffer; - import java.nio.ShortBuffer; - - import javax.microedition.khronos.egl.EGLConfig; - import javax.microedition.khronos.opengles.GL10; - - public class RectangleRender implements GLSurfaceView.Renderer { - //一个Float占用4Byte - private static final int BYTES_PER_FLOAT = 4; + public class SquareRender extends BaseGLSurfaceViewRenderer { //顶点位置缓存 private final FloatBuffer vertexBuffer; //顶点颜色缓存 private final FloatBuffer colorBuffer; - //渲染程序 - private int mProgram; - - //返回属性变量的位置 - //变换矩阵 - private int uMatrixLocation; //位置 private int aPositionLocation; //颜色 @@ -76,6 +189,7 @@ -0.5f, 0.5f, 0.5f, 0.5f, }; + /** * 颜色占用的向量个数 */ @@ -87,244 +201,126 @@ 0f, 1f, 1f, 0f, 1f, 1f, 0f, 0f }; - private final float[] mProjectionMatrix = new float[16]; - - public RectangleRender() { - //顶点位置相关 - //分配本地内存空间,每个浮点型占4字节空间;将坐标数据转换为FloatBuffer,用以传入给OpenGL ES程序 - vertexBuffer = ByteBuffer.allocateDirect(POINT_DATA.length * BYTES_PER_FLOAT) - .order(ByteOrder.nativeOrder()) - .asFloatBuffer(); - vertexBuffer.put(POINT_DATA); - vertexBuffer.position(0); - //顶点颜色相关 - colorBuffer = ByteBuffer.allocateDirect(COLOR_DATA.length * BYTES_PER_FLOAT) - .order(ByteOrder.nativeOrder()) - .asFloatBuffer(); - colorBuffer.put(COLOR_DATA); - colorBuffer.position(0); + public SquareRender() { + vertexBuffer = BufferUtil.getFloatBuffer(POINT_DATA); + colorBuffer = BufferUtil.getFloatBuffer(COLOR_DATA); } @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { - //将背景设置为白色 - GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f); - - //编译顶点着色程序 - String vertexShaderStr = ResReadUtils.readResource(R.raw.vertex_simple_shade); - int vertexShaderId = ShaderUtils.compileVertexShader(vertexShaderStr); - //编译片段着色程序 - String fragmentShaderStr = ResReadUtils.readResource(R.raw.fragment_simple_shade); - int fragmentShaderId = ShaderUtils.compileFragmentShader(fragmentShaderStr); - //连接程序 - mProgram = ShaderUtils.linkProgram(vertexShaderId, fragmentShaderId); - //在OpenGLES环境中使用程序 - GLES30.glUseProgram(mProgram); - - - uMatrixLocation = GLES30.glGetUniformLocation(mProgram, "u_Matrix"); - aPositionLocation = GLES30.glGetAttribLocation(mProgram, "vPosition"); - aColorLocation = GLES30.glGetAttribLocation(mProgram, "aColor"); + glClearColor(1.0f, 1.0f, 1.0f, 1.0f); + handleProgram(MyApplication.getInstance(), R.raw.iso_triangle_vertex_shader, R.raw.iso_triangle_fragment_shader); + aPositionLocation = glGetAttribLocation("vPosition"); + aColorLocation = glGetAttribLocation("aColor"); + glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GLES30.GL_FLOAT, false, 0, vertexBuffer); + glVertexAttribPointer(aColorLocation, COLOR_COMPONENT_COUNT, GLES30.GL_FLOAT, false, 0, colorBuffer); } @Override public void onSurfaceChanged(GL10 gl, int width, int height) { - //设置绘制窗口 - GLES30.glViewport(0, 0, width, height); - //正交投影方式 - final float aspectRatio = width > height ? - (float) width / (float) height : - (float) height / (float) width; - if (width > height) { - //横屏 - Matrix.orthoM(mProjectionMatrix, 0, -aspectRatio, aspectRatio, -1f, 1f, -1f, 1f); - } else { - //竖屏 - Matrix.orthoM(mProjectionMatrix, 0, -1f, 1f, -aspectRatio, aspectRatio, -1f, 1f); - } + glViewport(0, 0, width, height); + orthoM("u_Matrix", width, height); } @Override public void onDrawFrame(GL10 gl) { - //把颜色缓冲区设置为我们预设的颜色 - GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT); - - //将变换矩阵传入顶点渲染器 - GLES20.glUniformMatrix4fv(uMatrixLocation, 1, false, mProjectionMatrix, 0); - //准备坐标数据 - GLES30.glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GLES30.GL_FLOAT, false, 0, vertexBuffer); - //启用顶点位置句柄 - GLES30.glEnableVertexAttribArray(aPositionLocation); - - //准备颜色数据 - GLES30.glVertexAttribPointer(aColorLocation, COLOR_COMPONENT_COUNT, GLES30.GL_FLOAT, false, 0, colorBuffer); - //启用顶点颜色句柄 - GLES30.glEnableVertexAttribArray(aColorLocation); - // 开始绘制 - GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, POINT_DATA.length / POSITION_COMPONENT_COUNT); - //禁止顶点数组的句柄 - GLES30.glDisableVertexAttribArray(aPositionLocation); - GLES30.glDisableVertexAttribArray(aColorLocation); + glClear(GLES30.GL_COLOR_BUFFER_BIT); + glEnableVertexAttribArray(aPositionLocation); + glEnableVertexAttribArray(aColorLocation); + // 正方形、四个点(POINT_DATA.length / POSITION_COMPONENT_COUNT) + glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, POINT_DATA.length / POSITION_COMPONENT_COUNT); + glDisableVertexAttribArray(aPositionLocation); + glDisableVertexAttribArray(aColorLocation); } } ``` - 索引法: + 上面是一个正方形,那如果我画一个六边形,就需要多定义好几个顶点数据,来让其能组成对应的三角形拼接,下面是用索引法来画一个六边形,索引法相对于顶点法来说可以更高效,能节省很多顶点的数据: ```java - public class RectangleRender implements GLSurfaceView.Renderer { - //一个Float占用4Byte - private static final int BYTES_PER_FLOAT = 4; - //顶点个数 - private static final int POSITION_COMPONENT_COUNT = 4; + public class HexagonRender extends BaseGLSurfaceViewRenderer { //顶点位置缓存 private final FloatBuffer vertexBuffer; //顶点颜色缓存 - private final FloatBuffer colorBuffer; - //顶点索引缓存 - private final ShortBuffer indicesBuffer; - //渲染程序 - private int mProgram; - - //相机矩阵 - private final float[] mViewMatrix = new float[16]; - //投影矩阵 - private final float[] mProjectMatrix = new float[16]; - //最终变换矩阵 - private final float[] mMVPMatrix = new float[16]; - - //返回属性变量的位置 - //变换矩阵 - private int uMatrixLocation; + private final FloatBuffer colorBuffer; + // 顶点索引缓存 + private final ShortBuffer indexBuffer; //位置 private int aPositionLocation; //颜色 private int aColorLocation; - //四个顶点的位置参数 - private float rectangleCoords[] = { - -0.5f, 0.5f, 0.0f,//top left - -0.5f, -0.5f, 0.0f, // bottom left - 0.5f, -0.5f, 0.0f, // bottom right - 0.5f, 0.5f, 0.0f // top right + /** + * 坐标占用的向量个数 + */ + private static final int POSITION_COMPONENT_COUNT = 2; + private static final float[] POINT_DATA = { + -0.5f, -0.5f, + 0.5f, -0.5f, + 0.5f, 0.5f, + -0.5f, 0.5f, + 0f, -1.0f, + 0f, 1.0f }; /** - * 顶点索引 + * 数组绘制的索引:当前是绘制三角形,所以是3个元素构成一个绘制顺序 */ - private short[] indices = { - 0, 1, 2, 0, 2, 3 + private static final short[] INDEX_DATA = { + 0, 1, 2, + 0, 2, 3, + 0, 4, 1, + 3, 2, 5 }; - //四个顶点的颜色参数 - private float color[] = { - 0.0f, 0.0f, 1.0f, 1.0f,//top left - 0.0f, 1.0f, 0.0f, 1.0f,// bottom left - 0.0f, 0.0f, 1.0f, 1.0f,// bottom right - 1.0f, 0.0f, 0.0f, 1.0f// top right + /** + * 颜色占用的向量个数 + */ + private static final int COLOR_COMPONENT_COUNT = 4; + private static final float[] COLOR_DATA = { + // 一个顶点有3个向量数据:r、g、b、a + 1f, 0.5f, 0.5f, 0f, + 1f, 0f, 1f, 0f, + 0f, 1f, 1f, 0f, + 1f, 1f, 0f, 0f }; - public RectangleRender() { - //顶点位置相关 - //分配本地内存空间,每个浮点型占4字节空间;将坐标数据转换为FloatBuffer,用以传入给OpenGL ES程序 - vertexBuffer = ByteBuffer.allocateDirect(rectangleCoords.length * BYTES_PER_FLOAT) - .order(ByteOrder.nativeOrder()) - .asFloatBuffer(); - vertexBuffer.put(rectangleCoords); - vertexBuffer.position(0); - - //顶点颜色相关 - colorBuffer = ByteBuffer.allocateDirect(color.length * BYTES_PER_FLOAT) - .order(ByteOrder.nativeOrder()) - .asFloatBuffer(); - colorBuffer.put(color); - colorBuffer.position(0); - - //顶点索引相关 - indicesBuffer = ByteBuffer.allocateDirect(indices.length * 4) - .order(ByteOrder.nativeOrder()) - .asShortBuffer(); - indicesBuffer.put(indices); - indicesBuffer.position(0); + public HexagonRender() { + vertexBuffer = BufferUtil.getFloatBuffer(POINT_DATA); + colorBuffer = BufferUtil.getFloatBuffer(COLOR_DATA); + indexBuffer = BufferUtil.getShortBuffer(INDEX_DATA); } @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { - //将背景设置为白色 - GLES20.glClearColor(1.0f,1.0f,1.0f,1.0f); - - //编译顶点着色程序 - String vertexShaderStr = ResReadUtils.readResource(R.raw.vertex_simple_shade); - int vertexShaderId = ShaderUtils.compileVertexShader(vertexShaderStr); - //编译片段着色程序 - String fragmentShaderStr = ResReadUtils.readResource(R.raw.fragment_simple_shade); - int fragmentShaderId = ShaderUtils.compileFragmentShader(fragmentShaderStr); - //连接程序 - mProgram = ShaderUtils.linkProgram(vertexShaderId, fragmentShaderId); - //在OpenGLES环境中使用程序 - GLES30.glUseProgram(mProgram); - - - uMatrixLocation = GLES30.glGetUniformLocation(mProgram, "u_Matrix"); - aPositionLocation = GLES30.glGetAttribLocation(mProgram, "vPosition"); - aColorLocation = GLES30.glGetAttribLocation(mProgram, "aColor"); + glClearColor(1.0f, 1.0f, 1.0f, 1.0f); + handleProgram(MyApplication.getInstance(), R.raw.iso_triangle_vertex_shader, R.raw.iso_triangle_fragment_shader); + aPositionLocation = glGetAttribLocation("vPosition"); + aColorLocation = glGetAttribLocation("aColor"); + glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GLES30.GL_FLOAT, false, 0, vertexBuffer); + glVertexAttribPointer(aColorLocation, COLOR_COMPONENT_COUNT, GLES30.GL_FLOAT, false, 0, colorBuffer); } @Override public void onSurfaceChanged(GL10 gl, int width, int height) { - //设置绘制窗口 - GLES30.glViewport(0, 0, width, height); - - - //相机和透视投影方式 - //计算宽高比 - float ratio=(float)width/height; - //设置透视投影 - Matrix.frustumM(mProjectMatrix, 0, -ratio, ratio, -1, 1, 3, 7); - //设置相机位置 - Matrix.setLookAtM(mViewMatrix, 0, 0, 0, 7.0f, 0f, 0f, 0f, 0f, 1.0f, 0.0f); - //计算变换矩阵 - Matrix.multiplyMM(mMVPMatrix,0,mProjectMatrix,0,mViewMatrix,0); - - - /*//正交投影方式 - final float aspectRatio = width > height ? - (float) width / (float) height : - (float) height / (float) width; - if (width > height) { - //横屏 - Matrix.orthoM(mMVPMatrix, 0, -aspectRatio, aspectRatio, -1f, 1f, -1f, 1f); - } else { - //竖屏 - Matrix.orthoM(mMVPMatrix, 0, -1f, 1f, -aspectRatio, aspectRatio, -1f, 1f); - }*/ + glViewport(0, 0, width, height); + orthoM("u_Matrix", width, height); } @Override public void onDrawFrame(GL10 gl) { - //把颜色缓冲区设置为我们预设的颜色 - GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT); - - //将变换矩阵传入顶点渲染器 - GLES - 0.glUniformMatrix4fv(uMatrixLocation,1,false,mMVPMatrix,0); - //准备坐标数据 - GLES30.glVertexAttribPointer(aPositionLocation, 3, GLES30.GL_FLOAT, false, 0, vertexBuffer); - //启用顶点位置句柄 - GLES30.glEnableVertexAttribArray(aPositionLocation); - - //准备颜色数据 - GLES30.glVertexAttribPointer(aColorLocation, 4, GLES30.GL_FLOAT, false, 0, colorBuffer); - //启用顶点颜色句柄 - GLES30.glEnableVertexAttribArray(aColorLocation); - - //绘制三角形 - GLES30.glDrawElements(GL10.GL_TRIANGLES, indices.length, GL10.GL_UNSIGNED_SHORT, indicesBuffer); - - //禁止顶点数组的句柄 - GLES30.glDisableVertexAttribArray(aPositionLocation); - GLES30.glDisableVertexAttribArray(aColorLocation); + glClear(GLES30.GL_COLOR_BUFFER_BIT); + glEnableVertexAttribArray(aPositionLocation); + glEnableVertexAttribArray(aColorLocation); + // 绘制相对复杂的图形时,若顶点有较多重复时,对比数据占用空间而言,glDrawElements会比glDrawArrays小很多,也会更高效 + // 因为在有重复顶点的情况下,glDrawArrays方式需要的3个顶点位置是用Float型的,占3*4的Byte值; + // 而glDrawElements需要3个Short型的,占3*2Byte值 + // 1. 图形绘制方式; 2. 绘制的顶点数; 3. 索引的数据格式; 4. 索引的数据Buffer + glDrawElements(GLES30.GL_TRIANGLES, INDEX_DATA.length, + GLES30.GL_UNSIGNED_SHORT, indexBuffer); + glDisableVertexAttribArray(aPositionLocation); + glDisableVertexAttribArray(aColorLocation); } } ``` @@ -336,54 +332,31 @@ 其他也都和上面的一样,只有Render不同,如下: ```java -public class CircularRender implements GLSurfaceView.Renderer { - //一个Float占用4Byte - private static final int BYTES_PER_FLOAT = 4; +public class CircleRender extends BaseGLSurfaceViewRenderer { //顶点位置缓存 private final FloatBuffer vertexBuffer; //顶点颜色缓存 private final FloatBuffer colorBuffer; - //渲染程序 - private int mProgram; - - //相机矩阵 - private final float[] mViewMatrix = new float[16]; - //投影矩阵 - private final float[] mProjectMatrix = new float[16]; - //最终变换矩阵 - private final float[] mMVPMatrix = new float[16]; - - //返回属性变量的位置 - //变换矩阵 - private int uMatrixLocation; //位置 private int aPositionLocation; //颜色 private int aColorLocation; - //圆形顶点位置 - private float circularCoords[]; - //顶点的颜色 + /** + * 坐标占用的向量个数 + */ + private static final int POSITION_COMPONENT_COUNT = 3; + private float circlePosition[]; + /** + * 颜色占用的向量个数 + */ + private static final int COLOR_COMPONENT_COUNT = 4; private float color[]; - - public CircularRender() { - createPositions(1,60); - - //顶点位置相关 - //分配本地内存空间,每个浮点型占4字节空间;将坐标数据转换为FloatBuffer,用以传入给OpenGL ES程序 - vertexBuffer = ByteBuffer.allocateDirect(circularCoords.length * BYTES_PER_FLOAT) - .order(ByteOrder.nativeOrder()) - .asFloatBuffer(); - vertexBuffer.put(circularCoords); - vertexBuffer.position(0); - - //顶点颜色相关 - colorBuffer = ByteBuffer.allocateDirect(color.length * BYTES_PER_FLOAT) - .order(ByteOrder.nativeOrder()) - .asFloatBuffer(); - colorBuffer.put(color); - colorBuffer.position(0); + public CircleRender() { + createPositions(1, 60); + vertexBuffer = BufferUtil.getFloatBuffer(circlePosition); + colorBuffer = BufferUtil.getFloatBuffer(color); } private void createPositions(int radius, int n){ @@ -402,7 +375,7 @@ public class CircularRender implements GLSurfaceView.Renderer { f[i]=data.get(i); } - circularCoords = f; + circlePosition = f; //处理各个顶点的颜色 color = new float[f.length*4/3]; @@ -423,79 +396,28 @@ public class CircularRender implements GLSurfaceView.Renderer { @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { - //将背景设置为白色 - GLES30.glClearColor(1.0f,1.0f,1.0f,1.0f); - - //编译顶点着色程序 - String vertexShaderStr = ResReadUtils.readResource(R.raw.vertex_simple_shade); - int vertexShaderId = ShaderUtils.compileVertexShader(vertexShaderStr); - //编译片段着色程序 - String fragmentShaderStr = ResReadUtils.readResource(R.raw.fragment_simple_shade); - int fragmentShaderId = ShaderUtils.compileFragmentShader(fragmentShaderStr); - //连接程序 - mProgram = ShaderUtils.linkProgram(vertexShaderId, fragmentShaderId); - //在OpenGLES环境中使用程序 - GLES30.glUseProgram(mProgram); - - - uMatrixLocation = GLES30.glGetUniformLocation(mProgram, "u_Matrix"); - aPositionLocation = GLES30.glGetAttribLocation(mProgram, "vPosition"); - aColorLocation = GLES30.glGetAttribLocation(mProgram, "aColor"); + glClearColor(1.0f, 1.0f, 1.0f, 1.0f); + handleProgram(MyApplication.getInstance(), R.raw.iso_triangle_vertex_shader, R.raw.iso_triangle_fragment_shader); + aPositionLocation = glGetAttribLocation("vPosition"); + aColorLocation = glGetAttribLocation("aColor"); + glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GLES30.GL_FLOAT, false, 0, vertexBuffer); + glVertexAttribPointer(aColorLocation, COLOR_COMPONENT_COUNT, GLES30.GL_FLOAT, false, 0, colorBuffer); } @Override public void onSurfaceChanged(GL10 gl, int width, int height) { - //设置绘制窗口 - GLES30.glViewport(0, 0, width, height); - - - //相机和透视投影方式 - //计算宽高比 - float ratio=(float)width/height; - //设置透视投影 - Matrix.frustumM(mProjectMatrix, 0, -ratio, ratio, -1, 1, 3, 7); - //设置相机位置 - Matrix.setLookAtM(mViewMatrix, 0, 0, 0, 7.0f, 0f, 0f, 0f, 0f, 1.0f, 0.0f); - //计算变换矩阵 - Matrix.multiplyMM(mMVPMatrix,0,mProjectMatrix,0,mViewMatrix,0); - - - /*//正交投影方式 - final float aspectRatio = width > height ? - (float) width / (float) height : - (float) height / (float) width; - if (width > height) { - //横屏 - Matrix.orthoM(mMVPMatrix, 0, -aspectRatio, aspectRatio, -1f, 1f, -1f, 1f); - } else { - //竖屏 - Matrix.orthoM(mMVPMatrix, 0, -1f, 1f, -aspectRatio, aspectRatio, -1f, 1f); - }*/ + glViewport(0, 0, width, height); + orthoM("u_Matrix", width, height); } @Override public void onDrawFrame(GL10 gl) { - //把颜色缓冲区设置为我们预设的颜色 - GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT); - - //将变换矩阵传入顶点渲染器 - GLES30.glUniformMatrix4fv(uMatrixLocation,1,false,mMVPMatrix,0); - //准备坐标数据 - GLES30.glVertexAttribPointer(aPositionLocation, 3, GLES30.GL_FLOAT, false, 0, vertexBuffer); - //启用顶点位置句柄 - GLES30.glEnableVertexAttribArray(aPositionLocation); - - //准备颜色数据 - GLES30.glVertexAttribPointer(aColorLocation, 4, GLES30.GL_FLOAT, false, 0, colorBuffer); - //启用顶点颜色句柄 - GLES30.glEnableVertexAttribArray(aColorLocation); - - //绘制圆形 - GLES30.glDrawArrays(GLES30.GL_TRIANGLE_FAN, 0, circularCoords.length/3); - - //禁止顶点数组的句柄 - GLES30.glDisableVertexAttribArray(aPositionLocation); - GLES30.glDisableVertexAttribArray(aColorLocation); + glClear(GLES30.GL_COLOR_BUFFER_BIT); + glEnableVertexAttribArray(aPositionLocation); + glEnableVertexAttribArray(aColorLocation); + glDrawArrays(GLES30.GL_TRIANGLE_FAN, 0, circlePosition.length / POSITION_COMPONENT_COUNT); + glDisableVertexAttribArray(aPositionLocation); + glDisableVertexAttribArray(aColorLocation); } } ``` 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 008dc509..82df2b94 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" @@ -15,22 +15,23 @@ void main() } ``` +#version 300 es是表示GLSL语言的版本为300,对应的是OpenGL ES 3.0版本。 + + + 顶点着色器的输入包括: -- 属性:用顶点数组提供的逐顶点数据 -- 统一变量和统一变量缓冲区:顶点着色器使用的不变数据 -- 采样器:代表顶点着色器使用的纹理的特殊统一变量类型 -- 着色器程序:顶点着色器程序源代码或者描述在操作顶点的可执行文件 +- 顶点着色器输入(或者属性):用顶点数组提供的每个顶点的数据。 +- 统一变量(uniform):顶点或片段着色器使用的不变的数据。 +- 采样器:代表顶点着色器使用纹理的特殊统一变量类型。 +- 着色器程序:描述顶点上执行操作的顶点着色器的程序源代码或可执行文件。 -顶点着色器的输出称作顶点着色器输出变量。在图元光栅化阶段,为每个生成的片段计算这些变量,并作为片段着色器的输入传入。 +顶点着色器的输出在OpenGL ES 2.0中称作可变(varying)变量,但在OpenGL ES 3.0中改名为顶点着色器输出(out)变量。在图元光栅化阶段,为每个生成的片段计算顶点着色器输出值,并作为输入传递给片段着色器。 内建变量包括: -- gl_VertexID: 一个输入变量,用于保存顶点的整数索引。 -- gl_InstanceID:一个输入变量,用于保存实例化绘图调用中图元的实例编号。 - gl_Position:用于输出顶点位置的裁剪坐标。 - gl_PointSize:用于写入以像素标示的点尺寸。 -- gl_FrontFacing:一个特殊变量,但不是由顶点着色器直接写入的,而是根据顶点着色器生成的位置值和渲染的图元类型生成的。 ### 片段着色器: @@ -46,28 +47,41 @@ void main() } ``` + + 片段着色器为片段操作提供了通用功能的可编程方法。片段着色器的输入由以下部分组成: -- 输入(或者可变值):顶点着色器生成的插值数据。顶点着色器输出跨图元进行插值,并作为输入传递给片段着色器。 -- 统一变量:片段着色器使用的状态,这些常量值在每个片段上不会变化。 -- 采样器:用于访问着色器中的纹理图像。 -- 代码:片段着色器源代码或者二进制代码,描述在片段上执行的操作。 +- 输入变量:光栅化单元用插值为每个片段生成的顶点着色器输出。 +- 统一变量:片段(或者顶点)着色器使用的不变的数据。 +- 采样器:代表顶点着色器使用纹理的特殊统一变量类型。 +- 着色器程序:描述顶点上执行操作的顶点着色器的程序源代码或可执行文件。 片段着色器的输出是一个或者多个片段颜色,传递到管线的逐片段操作部分。 内建变量: gl_FragCoord:片段着色器中的一个只读变量。这个变量保存片段的窗口相对坐标。 -这里需要重点讲一下上面的版本: OpenGL ES版本有自己的着色器语言,其中OpenGL ES的版本与GLSL的版本有对应的关联关心: +这里需要重点讲一下上面的版本: 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) -- texture()方法代替了之前的texture2D()方法 +- 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的内置属性,变成我们需要自己使用out关键字定义的属性,这就是为什么你从网还是哪个找到的代码换成GLSL ES 300版本后报错的原因。 + +- 舍弃了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键,只能用空格替换。 @@ -122,9 +136,9 @@ GLSL中的数据类型主要分为标量、向量、矩阵、采样器、结构 - none:(默认的可省略)本地变量,可读可写,函数的输入参数既是这种类型 - const:常量 -- attribute:一般用于各个顶点各不相同的量。如顶点颜色、坐标等。用于保存顶点或法线数据,它可以在数据缓冲区中读取数据,仅能用于顶点着色器 +- in:输入变量,一般用于各个顶点各不相同的量。如顶点颜色、坐标等。用于保存顶点或法线数据,它可以在数据缓冲区中读取数据,仅能用于顶点着色器。(GLSL 100 es版本中是attribute) - uniform:统一变量。统一变量存储应用程序通过OpenGL ES API传入着色器的只读值。在运行时 shader 无法改变 uniform 变量,一般用来放置程序传递给 shader 的变换矩阵,材质,光照参数等等,可用于顶点着色器和片元着色器。如果统一变量在顶点着色器和片段着色器中均有声明,则声明的类型必须相同,且两个着色器中的值也需要相同。 -- varying:易变量,用于修饰从顶点着色器向片元着色器传递的变量。一般是在光栅化图元的过程中计算生成,记录在每个片段中(而不是从顶点着色器直接传递给片元着色器) +- out:输出变量,易变量,用于修饰从顶点着色器向片元着色器传递的变量。一般是在光栅化图元的过程中计算生成,记录在每个片段中(而不是从顶点着色器直接传递给片元着色器),(GLSL 100 es版本中是varying) 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 a820cc76..561cec8e 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" @@ -7,10 +7,11 @@ GLES作为我们与着色器连接的工具类提供了丰富的api。 上一篇文章说到GLSL中的变量修饰符有以下部分: - none:(默认的可省略)本地变量,可读可写,函数的输入参数既是这种类型 -- const:声明变量或函数的参数为只读类型 -- attribute:用于保存顶点或法线数据,它可以在数据缓冲区中读取数据,仅能用于顶点着色器(GLSL 200 es版本,在GLSL 300 es中已经被in替代) -- uniform:在运行时 shader 无法改变 uniform 变量,一般用来放置程序传递给 shader 的变换矩阵,材质,光照参数等等,可用于顶点着色器和片元着色器 -- varying:用于修饰从顶点着色器向片元着色器传递的变量(仅能用GLSL 200 es版本,在GLSL 300 es中已经被out替代) +- const:常量 +- in:输入变量,一般用于各个顶点各不相同的量。如顶点颜色、坐标等。用于保存顶点或法线数据,它可以在数据缓冲区中读取数据,仅能用于顶点着色器。 +- uniform:统一变量。统一变量存储应用程序通过OpenGL ES API传入着色器的只读值。在运行时 shader 无法改变 uniform 变量,一般用来放置程序传递给 shader 的变换矩阵,材质,光照参数等等,可用于顶点着色器和片元着色器。如果统一变量在顶点着色器和片段着色器中均有声明,则声明的类型必须相同,且两个着色器中的值也需要相同。 +- out:输出变量,易变量,用于修饰从顶点着色器向片元着色器传递的变量。一般是在光栅化图元的过程中计算生成,记录在每个片段中(而不是从顶点着色器直接传递给片元着色器) +- OpenGL ES 2.0(GLSL 100 es)版本中是属性和变量分别是attribute,varying,在OpenGL ES 3.0中已经被in和out替代。 了解一些常用API的意思能更方便后面的学习,这里就简单整理列了一些常用的部分: @@ -115,13 +116,13 @@ GLES作为我们与着色器连接的工具类提供了丰富的api。 ``` // 将最终变换矩阵传入shader程序 -GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, MatrixState.getFinalMatrix(), 0); +GLES30.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, MatrixState.getFinalMatrix(), 0); // 顶点位置数据传入着色器 -GLES20.glVertexAttribPointer(maPositionHandle, 3, GLES20.GL_FLOAT, false, 20, mRectBuffer); +GLES30.glVertexAttribPointer(maPositionHandle, 3, GLES20.GL_FLOAT, false, 20, mRectBuffer); // 顶点颜色数据传入着色器中 -GLES20.glVertexAttribPointer(maColorHandle, 4, GLES20.GL_FLOAT, false, 4*4, mColorBuffer); +GLES30.glVertexAttribPointer(maColorHandle, 4, GLES20.GL_FLOAT, false, 4*4, mColorBuffer); // 顶点坐标传递到顶点着色器 -GLES20.glVertexAttribPointer(maTextureHandle, 2, GLES20.GL_FLOAT, false, 20, mRectBuffer); +GLES30.glVertexAttribPointer(maTextureHandle, 2, GLES20.GL_FLOAT, false, 20, mRectBuffer); ``` ### 定义顶点属性数组 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 efae153f..ce046283 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" @@ -8,7 +8,7 @@ - 纹理Id:句柄,纹理的直接引用 -- 纹理单元:纹理的操作容器,有GL_TEXTURE0、GL_TEXTURE1、GL_TEXTURE2等,纹理单元的数量是有限的,最多16个。所以在最多只能同时操作16个纹理。在切换使用纹理单元的时候,使用glActiveTexture方法。也就是说OpenGL ES中内置了很多个纹理单元,并且是连续的,我们在使用的时候要选择其中一个,一般默认选择第一个(GLES_TEXTURE0),并且如果不选的话OpenGL默认激活的也就是第一个纹理单元。激活纹理单元后需要把它和纹理Id绑定,然后再通过GLES30.glUniform1i()方法传递给着色器中。 +- 纹理单元:纹理的操作容器,有GL_TEXTURE0、GL_TEXTURE1、GL_TEXTURE2等,纹理单元的数量是有限的,最多16个。所以在最多只能同时操作16个纹理。在切换使用纹理单元的时候,使用glActiveTexture方法。也就是说OpenGL ES中内置了很多个纹理单元,并且是连续的,我们在使用的时候要选择其中一个,一般默认选择第一个(GLES_TEXTURE0),并且如果不选的话OpenGL默认激活的也就是第一个纹理单元。采样器统一变量将加载一个指定纹理绑定的纹理单元的数值,例如,用数值0指定采样器表示从单元GL_TEXTURE0读取,指定数值1表示从GL_TEXTURE1读取,以此类推。激活纹理单元后需要把它和纹理Id绑定,然后再通过GLES30.glUniform1i()方法传递给着色器中的采样器。 - 纹理目标:一个纹理单元中包含了多个类型的纹理目标,有GL_TEXTURE_1D、GL_TEXTURE_2D、CUBE_MAP等。本章中,将纹理ID绑定到纹理单元0的GL_TEXTURE_2D纹理目标上,之后对纹理目标的操作都是对纹理Id对应的数据进行操作。 @@ -30,10 +30,6 @@ OpenGL中,2D纹理也有自己的坐标体系,取值范围在(0,0)到(1,1) ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_es_texture_position.jpg) -### 纹理尺寸 - -在OpenGL ES 2.0中规定,纹理的每个维度必须是2次幂,也就是2、4、8....等等,纹理的最大值上限通常比较大,例如2048*2048。 - ### 文件读取 OpenGL不能直接加载jpg或者png这类被编码的压缩格式,需要加载原始数据,也就是bitmap。我们在内置图片到工程中,应该将图片放到drawable-nodpi中,避免读取时被压缩,通过BitmapFactory解码读取图片时,要设置为非缩放的方式,即options.isScaled=false。 @@ -67,12 +63,37 @@ OpenGL不能直接加载jpg或者png这类被编码的压缩格式,需要加 下面是一个工具类方法,相对通用,能解决大部分需求,这个方法可以将内置的图片资源加载出对应的纹理ID。 ```java -/** - * 纹理加载助手类 - */ -public class TextureHelper { +public class TextureUtil { private static final String TAG = "TextureHelper"; + public static int createOESTextureId(){ + int[] textures = new int[1]; + GLES20.glGenTextures(1, textures, 0); + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textures[0]); + + GLES20.glTexParameterf( + GLES11Ext.GL_TEXTURE_EXTERNAL_OES, + GLES20.GL_TEXTURE_MIN_FILTER, + GLES20.GL_LINEAR + ); + GLES20.glTexParameterf( + GLES11Ext.GL_TEXTURE_EXTERNAL_OES, + GLES20.GL_TEXTURE_MAG_FILTER, + GLES20.GL_LINEAR + ); + GLES20.glTexParameteri( + GLES11Ext.GL_TEXTURE_EXTERNAL_OES, + GLES20.GL_TEXTURE_WRAP_S, + GLES20.GL_CLAMP_TO_EDGE + ); + GLES20.glTexParameteri( + GLES11Ext.GL_TEXTURE_EXTERNAL_OES, + GLES20.GL_TEXTURE_WRAP_T, + GLES20.GL_CLAMP_TO_EDGE + ); + + return textures[0]; + } /** * 根据资源ID获取相应的OpenGL纹理ID,若加载失败则返回0 *
必须在GL线程中调用 @@ -84,9 +105,7 @@ public class TextureHelper { GLES20.glGenTextures(1, textureObjectIds, 0); if (textureObjectIds[0] == 0) { - if (LoggerConfig.ON) { - Log.w(TAG, "Could not generate a new OpenGL texture object."); - } + Log.w(TAG, "Could not generate a new OpenGL texture object."); return bean; } @@ -97,19 +116,17 @@ public class TextureHelper { context.getResources(), resourceId, options); if (bitmap == null) { - if (LoggerConfig.ON) { - Log.w(TAG, "Resource ID " + resourceId + " could not be decoded."); - } + Log.w(TAG, "Resource ID " + resourceId + " could not be decoded."); // 加载Bitmap资源失败,删除纹理Id - GLES30.glDeleteTextures(1, textureObjectIds, 0); + GLES20.glDeleteTextures(1, textureObjectIds, 0); return bean; } // 2. 将纹理绑定到OpenGL对象上 - GLES30.glBindTexture(GLES20.GL_TEXTURE_2D, textureObjectIds[0]); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureObjectIds[0]); // 3. 设置纹理过滤参数:解决纹理缩放过程中的锯齿问题。若不设置,则会导致纹理为黑色 - GLES30.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR_MIPMAP_LINEAR); - GLES30.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR_MIPMAP_LINEAR); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); // 4. 通过OpenGL对象读取Bitmap数据,并且绑定到纹理对象上,之后就可以回收Bitmap对象 GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0); @@ -128,7 +145,7 @@ public class TextureHelper { bean.setHeight(bitmap.getHeight()); bitmap.recycle(); - // 7. 将纹理从OpenGL对象上解绑 + // 7. 将纹理从OpenGL对象上解绑,现在OpenGL已经完成了纹理的加载,不需要再绑定此纹理了,后面使用此纹理时通过纹理对象的ID即可 GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); // 所以整个流程中,OpenGL对象类似一个容器或者中间者的方式,将Bitmap数据转移到OpenGL纹理上 @@ -210,7 +227,7 @@ public class TextureHelper { in vec2 vTexCoord; out vec4 vFragColor; void main() { - // 100 es版本中是texture2D,texture函数会将传进来的纹理和坐标进行差值采样,输出到颜色缓冲区。 + // 100 es版本中是texture2D,texture函数会将传进来的纹理和坐标进行差值采样,输出到颜色缓冲区。 vFragColor = texture(uTextureUnit,vTexCoord); } ``` @@ -224,37 +241,27 @@ public class TextureHelper { 纹理的绘制: ``` - //激活纹理,设置当前活动的纹理单元为单元0 - GLES30.glActiveTexture(GLES30.GL_TEXTURE0); - //绑定纹理,将纹理id绑定到当前活动的纹理单元上 - GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureId); - // 将纹理单元传递片段着色器的u_TextureUnit - GLES20.glUniform1i(aTextureLocation, 0); + //激活纹理,设置当前活动的纹理单元为单元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,它是默认激活的,纹理单元的主要目的就是给着色器多一个使用的纹理。通过纹理单元赋值给采样器,我们可以一次绑定多个纹理,只要我们在使用的时候激活纹理。 ```java - public class TextureRender implements GLSurfaceView.Renderer { - //一个Float占用4Byte - private static final int BYTES_PER_FLOAT = 4; - //顶点位置缓存 - private final FloatBuffer vertexBuffer; - //顶点颜色缓存 + public class TextureRender extends BaseGLSurfaceViewRenderer { + private final FloatBuffer vertextBuffer; private final FloatBuffer textureBuffer; - //渲染程序 - private int mProgram; - //返回属性变量的位置 - //变换矩阵 - private int uMatrixLocation; - //位置 + private int textureId; private int aPositionLocation; - //颜色 private int aTextureLocation; - private int textureId; + private int uSamplerTextureLocation; /** * 坐标占用的向量个数 @@ -263,9 +270,9 @@ public class TextureHelper { // 逆时针顺序排列 private static final float[] POINT_DATA = { -0.5f, -0.5f, - 0.5f, -0.5f, -0.5f, 0.5f, 0.5f, 0.5f, + 0.5f, -0.5f, }; /** * 颜色占用的向量个数 @@ -274,100 +281,54 @@ public class TextureHelper { // 纹理坐标(s, t),t坐标方向和顶点y坐标反着 private static final float[] TEXTURE_DATA = { 0.0f, 1.0f, - 1.0f, 1.0f, 0.0f, 0.0f, - 1.0f, 0.0f + 1.0f, 0.0f, + 1.0f, 1.0f }; - private final float[] mProjectionMatrix = new float[16]; public TextureRender() { - //顶点位置相关 - //分配本地内存空间,每个浮点型占4字节空间;将坐标数据转换为FloatBuffer,用以传入给OpenGL ES程序 - vertexBuffer = ByteBuffer.allocateDirect(POINT_DATA.length * BYTES_PER_FLOAT) - .order(ByteOrder.nativeOrder()) - .asFloatBuffer(); - vertexBuffer.put(POINT_DATA); - vertexBuffer.position(0); - - //顶点颜色相关 - textureBuffer = ByteBuffer.allocateDirect(TEXTURE_DATA.length * BYTES_PER_FLOAT) - .order(ByteOrder.nativeOrder()) - .asFloatBuffer(); - textureBuffer.put(TEXTURE_DATA); - textureBuffer.position(0); + vertextBuffer = BufferUtil.getFloatBuffer(POINT_DATA); + textureBuffer = BufferUtil.getFloatBuffer(TEXTURE_DATA); } @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { - //将背景设置为白色 - GLES30.glClearColor(1.0f, 1.0f, 1.0f, 1.0f); - - //编译顶点着色程序 - String vertexShaderStr = ResReadUtils.readResource(R.raw.texture_vertex_simple_shade); - int vertexShaderId = ShaderUtils.compileVertexShader(vertexShaderStr); - //编译片段着色程序 - String fragmentShaderStr = ResReadUtils.readResource(R.raw.texture_fragment_simple_shade); - int fragmentShaderId = ShaderUtils.compileFragmentShader(fragmentShaderStr); - //连接程序 - mProgram = ShaderUtils.linkProgram(vertexShaderId, fragmentShaderId); - //在OpenGLES环境中使用程序 - GLES30.glUseProgram(mProgram); - - // 获取这三个属性在着色器中的属性位置 - uMatrixLocation = GLES30.glGetUniformLocation(mProgram, "u_Matrix"); - aPositionLocation = GLES30.glGetAttribLocation(mProgram, "vPosition"); - aTextureLocation = GLES30.glGetAttribLocation(mProgram, "aTextureCoord"); - // 将图片加载进来生成位图 - textureId = TextureHelper.loadTexture(MyApplication.getInstance(), R.drawable.img).getTextureId(); + 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(); + glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GLES30.GL_FLOAT, false, 0, vertextBuffer); + glVertexAttribPointer(aTextureLocation, TEXTURE_COMPONENT_COUNT, GLES30.GL_FLOAT, false, 0, textureBuffer); } @Override public void onSurfaceChanged(GL10 gl, int width, int height) { - //设置绘制窗口 - GLES30.glViewport(0, 0, width, height); - //正交投影方式 - final float aspectRatio = width > height ? - (float) width / (float) height : - (float) height / (float) width; - if (width > height) { - //横屏 - Matrix.orthoM(mProjectionMatrix, 0, -aspectRatio, aspectRatio, -1f, 1f, -1f, 1f); - } else { - //竖屏 - Matrix.orthoM(mProjectionMatrix, 0, -1f, 1f, -aspectRatio, aspectRatio, -1f, 1f); - } + glViewport(0, 0, width, height); + orthoM("u_Matrix", width, height); } @Override public void onDrawFrame(GL10 gl) { - //把颜色缓冲区设置为我们预设的颜色 - GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT); - - //将变换矩阵传入顶点渲染器 - GLES30.glUniformMatrix4fv(uMatrixLocation, 1, false, mProjectionMatrix, 0); - //传入坐标数据 - GLES30.glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GLES30.GL_FLOAT, false, 0, vertexBuffer); - //启用顶点位置句柄 - GLES30.glEnableVertexAttribArray(aPositionLocation); - - //传入颜色数据 - GLES30.glVertexAttribPointer(aTextureLocation, TEXTURE_COMPONENT_COUNT, GLES30.GL_FLOAT, false, 0, textureBuffer); - //启用顶点颜色句柄 - GLES30.glEnableVertexAttribArray(aTextureLocation); - - //激活纹理 - GLES30.glActiveTexture(GLES30.GL_TEXTURE0); - //绑定纹理 - GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureId); - // 把着色里面的采样器统一变量sample设置为0 - GLES20.glUniform1i(aTextureLocation, 0); - - - // 开始绘制 - GLES30.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, POINT_DATA.length / POSITION_COMPONENT_COUNT); - //禁止顶点数组的句柄 - GLES30.glDisableVertexAttribArray(aPositionLocation); - GLES30.glDisableVertexAttribArray(aTextureLocation); + glClear(GLES30.GL_COLOR_BUFFER_BIT); + /****************/ + // 中间这一部分代码的意思: 采样器统一变量将加载一个指定纹理绑定的纹理单元的数值, + // 例如,用数值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); + glDisableVertexAttribArray(aPositionLocation); + glDisableVertexAttribArray(aTextureLocation); } } ``` @@ -380,173 +341,140 @@ public class TextureHelper { - 单纹理单元,多次绘制 - 多次调用glDrawArrays绘制纹理顶点的方式来实现,这样就是一张一张的按先后顺序,一层一层的绘制到当前的一帧画面。着色器与上面的完全一致,唯一不同的是要提供两个顶点位置的坐标,然后分别用这两个坐标调用两次glDrawArrays进行绘制 + 多次调用glDrawArrays绘制纹理顶点的方式来实现,这样就是一张一张的按先后顺序,一层一层的绘制到当前的一帧画面。着色器与上面的完全一致,唯一不同的是要提供两个顶点位置的坐标,然后分别设置这两个坐标,并绑定两次纹理,然后调用两次glDrawArrays进行绘制。 - ``` - public class TextureRender implements GLSurfaceView.Renderer { - //一个Float占用4Byte - private static final int BYTES_PER_FLOAT = 4; - //顶点位置缓存 - private final FloatBuffer vertexBuffer; - private final FloatBuffer vertexBuffer2; - //顶点颜色缓存 + ```java + public class MultiTextureRender extends BaseGLSurfaceViewRenderer { + private final FloatBuffer vertextBuffer; + private final FloatBuffer vertextBuffer2; private final FloatBuffer textureBuffer; - //渲染程序 - private int mProgram; - //返回属性变量的位置 - //变换矩阵 - private int uMatrixLocation; - //位置 - private int aPositionLocation; - //颜色 - private int aTextureLocation; 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, + 1f, -1f, }; private static final float[] POINT_DATA2 = { -0.5f, -0.5f, - 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, - 1.0f, 1.0f, 0.0f, 0.0f, - 1.0f, 0.0f + 1.0f, 0.0f, + 1.0f, 1.0f }; - private final float[] mProjectionMatrix = new float[16]; - public TextureRender() { - //顶点位置相关 - //分配本地内存空间,每个浮点型占4字节空间;将坐标数据转换为FloatBuffer,用以传入给OpenGL ES程序 - vertexBuffer = ByteBuffer.allocateDirect(POINT_DATA.length * BYTES_PER_FLOAT) - .order(ByteOrder.nativeOrder()) - .asFloatBuffer(); - vertexBuffer.put(POINT_DATA); - vertexBuffer.position(0); - - vertexBuffer2 = ByteBuffer.allocateDirect(POINT_DATA2.length * BYTES_PER_FLOAT) - .order(ByteOrder.nativeOrder()) - .asFloatBuffer(); - vertexBuffer2.put(POINT_DATA2); - vertexBuffer2.position(0); - - //顶点颜色相关 - textureBuffer = ByteBuffer.allocateDirect(TEXTURE_DATA.length * BYTES_PER_FLOAT) - .order(ByteOrder.nativeOrder()) - .asFloatBuffer(); - textureBuffer.put(TEXTURE_DATA); - textureBuffer.position(0); + 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) { - //将背景设置为白色 - GLES30.glClearColor(1.0f, 1.0f, 1.0f, 1.0f); - - //编译顶点着色程序 - String vertexShaderStr = ResReadUtils.readResource(R.raw.texture_vertex_simple_shade); - int vertexShaderId = ShaderUtils.compileVertexShader(vertexShaderStr); - //编译片段着色程序 - String fragmentShaderStr = ResReadUtils.readResource(R.raw.texture_fragment_simple_shade); - int fragmentShaderId = ShaderUtils.compileFragmentShader(fragmentShaderStr); - //连接程序 - mProgram = ShaderUtils.linkProgram(vertexShaderId, fragmentShaderId); - //在OpenGLES环境中使用程序 - GLES30.glUseProgram(mProgram); - - uMatrixLocation = GLES30.glGetUniformLocation(mProgram, "u_Matrix"); - aPositionLocation = GLES30.glGetAttribLocation(mProgram, "vPosition"); - aTextureLocation = GLES30.glGetAttribLocation(mProgram, "aTextureCoord"); - textureId = TextureHelper.loadTexture(MyApplication.getInstance(), R.drawable.img).getTextureId(); - textureId2 = TextureHelper.loadTexture(MyApplication.getInstance(), R.drawable.img).getTextureId(); + 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) { - //设置绘制窗口 - GLES30.glViewport(0, 0, width, height); - //正交投影方式 - final float aspectRatio = width > height ? - (float) width / (float) height : - (float) height / (float) width; - if (width > height) { - //横屏 - Matrix.orthoM(mProjectionMatrix, 0, -aspectRatio, aspectRatio, -1f, 1f, -1f, 1f); - } else { - //竖屏 - Matrix.orthoM(mProjectionMatrix, 0, -1f, 1f, -aspectRatio, aspectRatio, -1f, 1f); - } + glViewport(0, 0, width, height); + orthoM("u_Matrix", width, height); } @Override public void onDrawFrame(GL10 gl) { - //把颜色缓冲区设置为我们预设的颜色 - GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT); - - //将变换矩阵传入顶点渲染器 - GLES30.glUniformMatrix4fv(uMatrixLocation, 1, false, mProjectionMatrix, 0); - //准备坐标数据 - GLES30.glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GLES30.GL_FLOAT, - false, 0, vertexBuffer); - //启用顶点位置句柄 - GLES30.glEnableVertexAttribArray(aPositionLocation); - - //准备颜色数据 - GLES30.glVertexAttribPointer(aTextureLocation, TEXTURE_COMPONENT_COUNT, GLES30.GL_FLOAT, - false, 0, textureBuffer); - //启用顶点颜色句柄 - GLES30.glEnableVertexAttribArray(aTextureLocation); - - //激活纹理 - GLES30.glActiveTexture(GLES30.GL_TEXTURE0); - //绑定纹理 - GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureId); - // 将纹理单元传递片段着色器的u_TextureUnit - GLES30.glUniform1i(aTextureLocation, 0); - // 开始绘制 - GLES30.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, POINT_DATA.length / POSITION_COMPONENT_COUNT); - - - //准备第二个纹理的坐标数据 + 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, vertexBuffer2); - //启用顶点位置句柄 - GLES30.glEnableVertexAttribArray(aPositionLocation); + false, 0, vertextBuffer2); //绑定纹理,前面已经激活了,就不用再调了 GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureId2); - GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, POINT_DATA.length / POSITION_COMPONENT_COUNT); - - //禁止顶点数组的句柄 - GLES30.glDisableVertexAttribArray(aPositionLocation); - GLES30.glDisableVertexAttribArray(aTextureLocation); + // 画第二次 + glDrawArrays(GLES30.GL_TRIANGLE_FAN, 0, POINT_DATA2.length / POSITION_COMPONENT_COUNT); + glDisableVertexAttribArray(aPositionLocation); + glDisableVertexAttribArray(aTextureLocation); } - } ``` - - + + - 多纹理单元,单词绘制 - OpenGL可以同时操作的纹理单元是16个,那么我们可以利用多个纹理单元来进行绘制同一个图层,从而达到目的。 + 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; + } + } + ``` + + + + From 31a2ee357e15cfa29d7b853668a95db35a24ae39 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 24 Mar 2020 20:40:58 +0800 Subject: [PATCH 011/213] update --- .../1.OpenGL\347\256\200\344\273\213.md" | 2 +- ...55\346\224\276\350\247\206\351\242\221.md" | 4 +- .../11.OpenGL ES\346\273\244\351\225\234.md" | 29 ++++++----- ...20\347\240\201\350\247\243\346\236\220.md" | 35 +++++++------ ....GLTextureView\345\256\236\347\216\260.md" | 14 +++--- ...66\344\270\211\350\247\222\345\275\242.md" | 20 ++------ ...42\345\217\212\345\234\206\345\275\242.md" | 3 +- ...45\231\250\350\257\255\350\250\200GLSL.md" | 2 +- ...\261\273\345\217\212Matrix\347\261\273.md" | 49 +++++++------------ .../9.OpenGL ES\347\272\271\347\220\206.md" | 4 +- 10 files changed, 66 insertions(+), 96 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 30041c0d..ad360a9c 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" @@ -224,7 +224,7 @@ Android 框架中有如下两个基本类,用于通过 OpenGL ES API 来创建 -参考 +### 参考 --- 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 a9ab7d17..da220e6f 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" @@ -17,7 +17,7 @@ GLSurfaceView -> setRender -> onSurfaceCreated回调方法中构造一个Surface - 顶点着色器 - ```xml + ```glsl #version 300 es in vec4 vPosition; in vec2 vCoordPosition; @@ -33,7 +33,7 @@ GLSurfaceView -> setRender -> onSurfaceCreated回调方法中构造一个Surface 这里需要注意一下,就是做相机预览和视频播放的话,纹理的类型需要使用samplerExternalOES,而不是之前渲染图片的sampler2D。这是因为相机和视频的数据是YUV的,而OpenGL ES是RGB的,samplerExternalOES内部会进行处理。#extension用于启用和设置扩展的行为。格式为#extension all : behavior。behavior的可选值有: require、enable、warn、disable。 - ```xml + ```glsl #version 300 es #extension GL_OES_EGL_image_external_essl3 : require precision mediump float; 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 6f2c3839..4d0adb8f 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" @@ -38,20 +38,6 @@ void main(){ - - - - -滤镜的编写主要是靠基类。 - -- 先抽取BaseFilter类,封装好每个滤镜的着色器及onDraw等方法 -- 不同的滤镜集成该BaseFilter类 -- 在Renderder接口的实现类中去创建不同的Filter类,然后在onDrawFrame方法中去调用该filter.onDraw方法 - -滤镜的不同大部分只需要修改片元着色器就可以了。 - - - ### 反色滤镜 RGB三个通道的颜色取反,而alpha通道不变。 @@ -64,7 +50,7 @@ RGB三个通道的颜色取反,而alpha通道不变。 纹理默认传入的读取范围是(0,0)到(1,1)内的颜色值。如果对读取的位置进行调整修改,那么就可以做出各种各样的效果,例如缩放动画就是让读取的范围改成(-1,-1)到(2,2)。 -## 看完了疯了是不是,要做个滤镜效果,各种计算我实在弄不明白 +### 看完了疯了是不是,要做个滤镜效果,各种计算我实在弄不明白 最简单的方法就是通过LUT方法,通过设计师提供的LUT文件来实现预定的滤镜效果。基本思路如下: @@ -86,9 +72,22 @@ RGB三个通道的颜色取反,而alpha通道不变。 +## 视频播放滤镜实现 +滤镜的编写主要是靠基类。 + +- 先抽取BaseFilter类,封装好每个滤镜的着色器及onDraw等方法。 +- 不同的滤镜都继承该BaseFilter类,然后实现有区别的部分。其实每个不同滤镜的区别就是着色器的不同。 +- 在Renderder接口的实现类中去创建不同的Filter类,然后在onDrawFrame方法中去调用该filter.onDraw方法。 + +滤镜的不同大部分只需要修改片元着色器就可以了。 +### 滤镜的基类 +- onCreate():创建 +- onSizeChange():滤镜尺寸改变 +- onDraw():绘制每一帧 +- onDestroy():销毁,用于资源回收。 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 d9cbc25a..d5c9e8ab 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" @@ -2,7 +2,7 @@ 我感觉还是先看一下源码,了解一下内部的流程,再接着学习其他的OpenGL部分会更合适。 -从上一篇文章中GLSurfaceView的使用中可以看到入口是setRenderer()方法,这里就卡一下setRenderder(Renderer renderer)方法的实现: +从上一篇文章中GLSurfaceView的使用中可以看到入口是setRenderer()方法,这里就看一下setRenderder(Renderer renderer)方法的实现: - Renderer接口 @@ -50,7 +50,7 @@ public void setRenderer(Renderer renderer) { super(); mWidth = 0; mHeight = 0; - // 连续渲染模式下是true,如果是按需渲染模式就为false,然后通过requesetRender()方法来修改它的值 + // 连续渲染模式下是true,如果是按需渲染模式就为false,然后通过requesetRender()方法来修改它的值 mRequestRender = true; // 默认的渲染模式 mRenderMode = RENDERMODE_CONTINUOUSLY; @@ -74,7 +74,7 @@ public void setRenderer(Renderer renderer) { sGLThreadManager.threadExiting(this); } } - // GLSurfaceView的onPause方法会调用到这里 + // GLSurfaceView的onPause方法会调用到这里 public void onPause() { synchronized (sGLThreadManager) { if (LOG_PAUSE_RESUME) { @@ -97,7 +97,7 @@ public void setRenderer(Renderer renderer) { } } } - // GLSurfaceView的onResume方法会调用到这里 + // GLSurfaceView的onResume方法会调用到这里 public void onResume() { synchronized (sGLThreadManager) { if (LOG_PAUSE_RESUME) { @@ -135,7 +135,7 @@ public void setRenderer(Renderer renderer) { ```java private void guardedRun() throws InterruptedException { - //GL帮助类,内部建立GL环境 + //GL帮助类,内部建立GL环境 mEglHelper = new EglHelper(mGLSurfaceViewWeakRef); mHaveEglContext = false; mHaveEglSurface = false; @@ -159,16 +159,15 @@ public void setRenderer(Renderer renderer) { while (true) { synchronized (sGLThreadManager) { while (true) { - // 外部请求退出 + // 外部请求退出 if (mShouldExit) { return; - } - // 如果还有GL线程中要处理的事件没处理完,就先处理事件 + // 如果还有GL线程中要处理的事件没处理完,就先处理事件 if (! mEventQueue.isEmpty()) { event = mEventQueue.remove(0); break; } - // 更新onResume和onPause时的状态变化 + // 更新onResume和onPause时的状态变化 // Update the pause state. boolean pausing = false; if (mPaused != mRequestPaused) { @@ -180,7 +179,7 @@ public void setRenderer(Renderer renderer) { Log.i("GLThread", "mPaused is now " + mPaused + " tid=" + getId()); } } - // 需要释放EGLContext + // 需要释放EGLContext // Do we need to give up the EGL context? if (mShouldReleaseEglContext) { if (LOG_SURFACE) { @@ -191,14 +190,14 @@ public void setRenderer(Renderer renderer) { mShouldReleaseEglContext = false; askedToReleaseEglContext = true; } - // 如果EGLContext丢失,需要销毁EGLSurface和EGLContext + // 如果EGLContext丢失,需要销毁EGLSurface和EGLContext // Have we lost the EGL context? if (lostEglContext) { stopEglSurfaceLocked(); stopEglContextLocked(); lostEglContext = false; } - // 如果onPause了并且当前GLSurface已经存在了,就销毁EGLSurface + // 如果onPause了并且当前GLSurface已经存在了,就销毁EGLSurface // When pausing, release the EGL surface: if (pausing && mHaveEglSurface) { if (LOG_SURFACE) { @@ -206,7 +205,7 @@ public void setRenderer(Renderer renderer) { } stopEglSurfaceLocked(); } - // 接受了onPuase信号,并且当前EGLContext存在时,需要根据用户的设置来决定是否销毁EGLContext + // 接受了onPuase信号,并且当前EGLContext存在时,需要根据用户的设置来决定是否销毁EGLContext // When pausing, optionally release the EGL Context: if (pausing && mHaveEglContext) { GLSurfaceView view = mGLSurfaceViewWeakRef.get(); @@ -256,10 +255,10 @@ public void setRenderer(Renderer renderer) { finishDrawingRunnable = mFinishDrawingRunnable; mFinishDrawingRunnable = null; } - // readyToDraw()方法内部会判断是否已经pause或者mRequestRender是否是true以及是否是主动渲染模式。如果是被动渲染的模式mRequestRender就会是false,只有调用requestRender()方法后才会是true,如果是主动渲染模式readyToDraw()就不用根据mRequestRender来判断。 + // readyToDraw()方法内部会判断是否已经pause或者mRequestRender是否是true以及是否是主动渲染模式。如果是被动渲染的模式mRequestRender就会是false,只有调用requestRender()方法后才会是true,如果是主动渲染模式readyToDraw()就不用根据mRequestRender来判断。 // Ready to draw? if (readyToDraw()) { - // 没有EGLContext就去调用EGLHelper来创建,第一次会走到这里先创建EGLContext + // 没有EGLContext就去调用EGLHelper来创建,第一次会走到这里先创建EGLContext // If we don't have an EGL context, try to acquire one. if (! mHaveEglContext) { if (askedToReleaseEglContext) { @@ -332,7 +331,7 @@ public void setRenderer(Renderer renderer) { } if (createEglSurface) { - // EglHelper创建EglSurface + // EglHelper创建EglSurface if (mEglHelper.createSurface()) { synchronized(sGLThreadManager) { mFinishedCreatingEglSurface = true; @@ -554,7 +553,7 @@ public void setRenderer(Renderer renderer) { */ GLSurfaceView view = mGLSurfaceViewWeakRef.get(); if (view != null) { - // 也是通过EGLWindowSurfaceFactory来创建EGLSurface,如果没有设置就会调用默认的DefaultWindowSurfaceFactory。后面说一下这里 + // 也是通过EGLWindowSurfaceFactory来创建EGLSurface,如果没有设置就会调用默认的DefaultWindowSurfaceFactory。后面说一下这里 mEglSurface = view.mEGLWindowSurfaceFactory.createWindowSurface(mEgl, mEglDisplay, mEglConfig, view.getHolder()); } else { @@ -619,7 +618,7 @@ public void setRenderer(Renderer renderer) { EGL10.EGL_NO_CONTEXT); GLSurfaceView view = mGLSurfaceViewWeakRef.get(); if (view != null) { - // EGLWindowSurfaceFactory除了创建EGLSurface还有销毁的功能 + // EGLWindowSurfaceFactory除了创建EGLSurface还有销毁的功能 view.mEGLWindowSurfaceFactory.destroySurface(mEgl, mEglDisplay, mEglSurface); } mEglSurface = null; 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 195b9e83..86affc03 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" @@ -12,14 +12,14 @@ ``` - ```java - @Override - public void setSurfaceTextureListener(SurfaceTextureListener listener) { - if (mRenderer == null) { - // 与普通TextureView一样 - super.setSurfaceTextureListener(listener); - } - Log.e(TAG, "setSurfaceTextureListener preserved, setRenderer() instead?"); + @Override + public void setSurfaceTextureListener(SurfaceTextureListener listener) { + if (mRenderer == null) { + // 与普通TextureView一样 + super.setSurfaceTextureListener(listener); } + Log.e(TAG, "setSurfaceTextureListener preserved, setRenderer() instead?"); + } ``` ```java 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 624f5d0a..6e9adc89 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" @@ -152,17 +152,6 @@ Preferences -> Plugins -> 搜GLSL Support安装就可以了。 -### 编写GLSurfaceView.Render类 - -主要有以下功能: - -1. 声明绘制图形的坐标和颜色数据 -2. 为顶点位置及颜色申请本地内存 -3. 加载编译顶点着色器和片段着色器 -4. 创建program,连接顶点和片段着色器并链接program -5. 设置窗口大小 -6. 完成绘制 - ```java public class TriangleActivity extends Activity { private GLSurfaceView mGlSurfaceView; @@ -438,7 +427,7 @@ public class TriangleRender implements GLSurfaceView.Renderer { 效果如下: -![](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_es_tri.jpg) + 我们设置的是数据来看,应该是等腰三角形,但是实际效果并不是,这是因为前面说到的OpenGL ES使用的是虚拟坐标导致的。如果想让绘制一个等腰三角形该怎么做呢? @@ -499,8 +488,7 @@ int rmOffset, //变换矩阵的起始位置(偏移量) float eyeX,float eyeY, float eyeZ, //相机位置 float centerX,float centerY,float centerZ, //观测点位置 float upX,float upY,float upZ) //up向量在xyz上的分量) { - -------------省略代码------------- + ... } ``` @@ -537,7 +525,7 @@ Android OpenGL ES的投影分为两种: float top,//相对观察点近面的上边距 float near,//相对观察点近面距离 float far) //相对观察点远面距离{ - ---------------省略代码-------------- + ... } ``` @@ -569,7 +557,7 @@ Android OpenGL ES的投影分为两种: float top, //相对观察点近面的上边距 float near, //相对观察点近面距离 float far) //相对观察点远面距离 { - ---------------省略代码-------------- + ... } ``` 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 a9ed80f0..37e1661c 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" @@ -422,8 +422,7 @@ 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) +[上一篇: 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 82df2b94..7e935fbd 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" @@ -203,7 +203,7 @@ GLSL的类型转换与C不同。在GLSL中类型不可以自动提升,比如fl - radians(x):角度转弧度 - degrees(x):弧度转角度 - sin(x):正弦函数,传入值为弧度。相同的还有cos余弦函数、tan正切函数、asin反正弦、acos反余弦 -- atan反正切 +- atan():反正切 - pow(x,y):xy - exp(x):ex - exp2(x):2x 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 561cec8e..2eb24927 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" @@ -2,18 +2,7 @@ -GLES作为我们与着色器连接的工具类提供了丰富的api。 - -上一篇文章说到GLSL中的变量修饰符有以下部分: - -- none:(默认的可省略)本地变量,可读可写,函数的输入参数既是这种类型 -- const:常量 -- in:输入变量,一般用于各个顶点各不相同的量。如顶点颜色、坐标等。用于保存顶点或法线数据,它可以在数据缓冲区中读取数据,仅能用于顶点着色器。 -- uniform:统一变量。统一变量存储应用程序通过OpenGL ES API传入着色器的只读值。在运行时 shader 无法改变 uniform 变量,一般用来放置程序传递给 shader 的变换矩阵,材质,光照参数等等,可用于顶点着色器和片元着色器。如果统一变量在顶点着色器和片段着色器中均有声明,则声明的类型必须相同,且两个着色器中的值也需要相同。 -- out:输出变量,易变量,用于修饰从顶点着色器向片元着色器传递的变量。一般是在光栅化图元的过程中计算生成,记录在每个片段中(而不是从顶点着色器直接传递给片元着色器) -- OpenGL ES 2.0(GLSL 100 es)版本中是属性和变量分别是attribute,varying,在OpenGL ES 3.0中已经被in和out替代。 - -了解一些常用API的意思能更方便后面的学习,这里就简单整理列了一些常用的部分: +GLES作为我们与着色器连接的工具类提供了丰富的api。了解一些常用API的意思能更方便后面的学习,这里就简单整理列了一些常用的部分: - glClearColor(GLclampf red, GLclampf green, GLclampf blue, GLclampf alpha) @@ -73,7 +62,7 @@ GLES作为我们与着色器连接的工具类提供了丰富的api。 - void glGetVertexAttribPointerv(GLunit index, GLenum pname, GLvoid **pointer) - 已**pointer返回指定的通用顶点属性的指针信息。参数index为要返回单额通用顶点属性采参数,pname是指定要返回的通用顶点属性参数的符号名称,必须是GL_VERTEX_ATTRIB_ARRAY_POINTER。 + 以**pointer返回指定的通用顶点属性的指针信息。参数index为要返回单额通用顶点属性采参数,pname是指定要返回的通用顶点属性参数的符号名称,必须是GL_VERTEX_ATTRIB_ARRAY_POINTER。 - glLinkProgram(GLunit program) @@ -145,21 +134,11 @@ GLES30.glVertexAttribPointer(maTextureHandle, 2, GLES20.GL_FLOAT, false, 20, mRe 调用GLES30.glEnableVertexAttribArray和GLES30.glDisableVertexAttribArray传入参数index。如果启用,那么当GLES30.glDrawArrays或者GLES30.glDrawElements被调用时,顶点属性数组会被使用。 -### 选择活动纹理单元 - -``` -void glActiveTexture(int texture) -``` - -texture指定哪一个纹理单元被置为活动状态。texture必须是GL_TEXTUREi之一,其中0 <= i < GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS,初始值为GL_TEXTURE0。 -GLES20.glActiveTexture()确定了后续的纹理状态改变影响哪个纹理,纹理单元的数量是依据该纹理单元所被支持的具体实现。 - ## Matrix -而`Matrix就是专门设计出来帮助我们简化矩阵和向量运算操作的,里面所有的实现原理都是线性代数中的运算`。 +Matrix就是专门设计出来帮助我们简化矩阵和向量运算操作的,里面所有的实现原理都是线性代数中的运算。 -我们知道OpenGl中实现图形的操作大量使用了矩阵,在OpenGL中使用的向量为列向量,我们通过利用矩阵与列向量(颜色、坐标都可看做列向量)相乘,得到一个新的列向量。利用这点,我们构建一个的矩阵,与图形所有的顶点坐标坐标相乘,得到新的顶点坐标集合,当这个矩阵构造恰当的话,新得到的顶点坐标集合形成的图形相对原图形就会出现平移、旋转、缩放或拉伸、抑或扭曲的效果。 -`Matrix:专门为处理4*4矩阵和4元素向量设计的,其中的方法都是static的,不需要初始化Matrix实例`。 +我们知道OpenGl中实现图形的操作大量使用了矩阵,在OpenGL中使用的向量为列向量,我们通过利用矩阵与列向量(颜色、坐标都可看做列向量)相乘,得到一个新的列向量。利用这点,我们构建一个的矩阵,与图形所有的顶点坐标坐标相乘,得到新的顶点坐标集合,当这个矩阵构造恰当的话,新得到的顶点坐标集合形成的图形相对原图形就会出现平移、旋转、缩放或拉伸、抑或扭曲的效果。Matrix是专门为处理4*4矩阵和4元素向量设计的,其中的方法都是static的,不需要初始化Matrix实例。 - multiplyMM @@ -204,19 +183,25 @@ GLES20.glActiveTexture()确定了后续的纹理状态改变影响哪个纹理 计算向量长度 -- setldentityM +- setIdentityM(float[] sm, int smOffset) + + 用来创建一个单位矩阵。其中第一个参数是创建出来的单位矩阵存储的地方,是一个float类型的一维数组。第二个参数是存储的数据位置的偏移量,也就是说从哪里开始存储。生成的结果先按照列优先存储的,也就是说先存放第一列的数据,再存放第二列的数据,以此类推。前面理论部分已经提到,所有变换都是基于单位矩阵的基础上进行的,所以第一步创建单位矩阵是必须的。 - 创建单位矩阵 +- scaleM(float[] m, int mOffset, + float x, float y, float z) -- scaleM + 用来进行图像的缩放,第一个参数是需要变换的矩阵;第三、四、五个参数分别对应x,y,z 方向的缩放比例,当x方向缩放为0.5时,相当于向x方向缩放为原来的0.5倍,其他类似。 -- translateM +- translateM( + float[] m, int mOffset, + float x, float y, float z) -- rotateM + 用来进行图像的位移,第一个参数是需要变换的矩阵;第二个参数是偏移量;第三、四、五个参数分别对应x,y,z 方向的位移量。其以图像自身x,y,z方向为单位,也就是说当x方向位移量为0.5时,相当于向右移动0.5个身位,其他类似。 -- setRotateM +- rotateM(float[] m, int mOffset, + float a, float x, float y, float z) -- setRotateEulerM + 用来进行旋转变换的。第一个参数是需要变换的矩阵;第二参数是偏移量;第三个参数是旋转角度,这边是以角度制,也就是说是0-360这个范围;第四、五、六个参数分别代表旋转轴向量的x,y,z值。如果x=0,y=0,z = 1 就相当于以z轴为旋转轴进行旋转,其他类似。 - setLookAtm 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 ce046283..962345b9 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" @@ -8,7 +8,7 @@ - 纹理Id:句柄,纹理的直接引用 -- 纹理单元:纹理的操作容器,有GL_TEXTURE0、GL_TEXTURE1、GL_TEXTURE2等,纹理单元的数量是有限的,最多16个。所以在最多只能同时操作16个纹理。在切换使用纹理单元的时候,使用glActiveTexture方法。也就是说OpenGL ES中内置了很多个纹理单元,并且是连续的,我们在使用的时候要选择其中一个,一般默认选择第一个(GLES_TEXTURE0),并且如果不选的话OpenGL默认激活的也就是第一个纹理单元。采样器统一变量将加载一个指定纹理绑定的纹理单元的数值,例如,用数值0指定采样器表示从单元GL_TEXTURE0读取,指定数值1表示从GL_TEXTURE1读取,以此类推。激活纹理单元后需要把它和纹理Id绑定,然后再通过GLES30.glUniform1i()方法传递给着色器中的采样器。 +- 纹理单元:纹理的操作容器,有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对应的数据进行操作。 @@ -221,7 +221,7 @@ public class TextureUtil { ``` #version 300 es precision mediump float; - // 采样器(sampler)是用于从纹理贴图读取的特殊统一变量。采样器统一变量将加载一个指定纹理绑定的纹理单元额数据,java代码里面需要把它设置为0 + // 采样器(sampler)是用于从纹理贴图读取的特殊统一变量。采样器统一变量将加载一个指定纹理绑定的纹理单元额数据,java代码里面需要把它设置为纹理单元对应的index值。 uniform sampler2D uTextureUnit; //接收刚才顶点着色器传入的纹理坐标(s,t) in vec2 vTexCoord; From e17d6201c8ee6abd29d6d60a5ee5b805e59741dd Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 25 Mar 2020 21:50:53 +0800 Subject: [PATCH 012/213] update --- "JavaKnowledge/Git\347\256\200\344\273\213.md" | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git "a/JavaKnowledge/Git\347\256\200\344\273\213.md" "b/JavaKnowledge/Git\347\256\200\344\273\213.md" index f98977be..98287ca2 100644 --- "a/JavaKnowledge/Git\347\256\200\344\273\213.md" +++ "b/JavaKnowledge/Git\347\256\200\344\273\213.md" @@ -259,6 +259,14 @@ git push // 把所有文件从本地仓库推送进远程仓库 执行`git reset --hard origin/master`还是`git reset --hard`命令,只不过这次多了一个参数`origin/master`,这代表远程仓库,既然本地仓库已经有了 你提交的脏代码,那么就从远程仓库中把代码恢复把。 + 但是上面这样会导致你之前修改的代码都没有了,如果我只是想撤回提交,还想要我之前修改的东西重新回到本地仓库呢? + `git reset --soft HEAD^`,这样就成功的撤销了你的commit。注意,仅仅是撤回commit操作,您写的代码仍然保留。 + + 至于这几个参数: + 1. --mixed:不删除工作空间改动代码,撤销commit,并且撤销git add . 操作这个为默认参数,git reset --mixed HEAD^ 和 git reset HEAD^ 效果是一样的。 + 2. --soft:不删除工作空间改动代码,撤销commit,不撤销git add . + 3. --hard:删除工作空间改动代码,撤销commit,撤销git add . 注意完成这个操作后,就恢复到了上一次的commit状态。 + - 已推送到远程仓库 如果你执行`git add .`后又`commit`又执行了`git push`操作了,这时候你的代码已经进入到了远程仓库中,如果你发现你提交的代码又问题想恢复的话,那你只能先把本地仓库的 代码恢复,然后再强制执行`git push`仓做,`push`到远程仓库就可以了。 From 2075581d87f30eb2de51b83688c550f2b5255a3c Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 26 Mar 2020 21:23:50 +0800 Subject: [PATCH 013/213] update --- ...55\346\224\276\350\247\206\351\242\221.md" | 184 +++++++++++++++++- .../11.OpenGL ES\346\273\244\351\225\234.md" | 2 +- 2 files changed, 178 insertions(+), 8 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 da220e6f..88ca3e35 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" @@ -58,13 +58,8 @@ public class VideoPlayerActivity extends Activity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - mGLSurfaceView = new GLSurfaceView(this); - Display display = getWindowManager().getDefaultDisplay(); - int width = display.getWidth(); - int height = (int) ((width) * (9 / 16f)); - ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(width, height); - mGLSurfaceView.setLayoutParams(layoutParams); - setContentView(mGLSurfaceView); + setContentView(R.layout.activity_video_player); + mGLSurfaceView = findViewById(R.id.mGLSurfaceView); mGLSurfaceView.setEGLContextClientVersion(3); VideoPlayerRender videoPlayerRender = new VideoPlayerRender(mGLSurfaceView); videoPlayerRender.setIVideoTextureRenderListener(new VideoPlayerRender.IVideoTextureRenderListener() { @@ -149,9 +144,184 @@ public class VideoPlayerActivity extends Activity { ### 修复变形 +我们要根据视频的宽高和Render中Surface的宽高来计算缩放的比例,让高度或者宽度进行缩放,这样就不会变形了。主要修改的地方就是通过换算这个比例然后根据这个比例来改变顶点渲染器中的点的坐标值,从而让其在换算后的坐标内开始画面,这样就不会变形了。 + +下面是改动后的VideoPlayerRender类: + +```java +public class VideoPlayerRender extends BaseGLSurfaceViewRenderer { + private int mTextureId; + private SurfaceTexture mSurfaceTexture; + private GLSurfaceView mGLSurfaceView; + private boolean mUpdateSurfaceTexture; + private FloatBuffer mVertextBuffer; + private FloatBuffer mTextureBuffer; + private int vertexPosition; + private int texturePosition; + private int samplerTexturePosition; + /** + * 视频的宽高 + */ + private int mVideoWidth; + private int mVideoHeight; + /** + * 需改更改渲染的大小 + */ + private boolean mNeedUpdateSize; + /** + * Surface的宽高 + */ + private int mSurfaceWidth; + private int mSurfaceHeight; + + /** + * 坐标占用的向量个数 + */ + 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 int TEXTURE_COMPONENT_COUNT = 2; + // 纹理坐标(s, t),t坐标方向和顶点y坐标反着 + private static final float[] TEXTURE_DATA = { + 0.0f, 1.0f, + 1.0f, 1.0f, + 0.0f, 0.0f, + 1.0f, 0.0f + }; + + public VideoPlayerRender(GLSurfaceView surfaceView) { + mGLSurfaceView = surfaceView; + mVertextBuffer = BufferUtil.getFloatBuffer(POINT_DATA); + mTextureBuffer = BufferUtil.getFloatBuffer(TEXTURE_DATA); + } + + @Override + public void onSurfaceCreated(GL10 gl, EGLConfig config) { + glClearColor(0.0f, 1.0f, 0.0f, 1.0f); + handleProgram(MyApplication.getInstance(), R.raw.video_vertex_shader, R.raw.video_fragment_shader); + vertexPosition = glGetAttribLocation("vPosition"); + texturePosition = glGetAttribLocation("vCoordPosition"); + samplerTexturePosition = glGetUniformLocation("uSamplerTexture"); + mTextureId = TextureUtil.createOESTextureId(); + mSurfaceTexture = new SurfaceTexture(mTextureId); + mSurfaceTexture.setDefaultBufferSize(mGLSurfaceView.getWidth(), mGLSurfaceView.getHeight()); + mSurfaceTexture.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() { + @Override + public void onFrameAvailable(SurfaceTexture surfaceTexture) { + mUpdateSurfaceTexture = true; + if (mGLSurfaceView != null) { + mGLSurfaceView.requestRender(); + } + } + }); + if (mTextureRenderListener != null) { + mTextureRenderListener.onCreate(mSurfaceTexture); + } + } + + @Override + public void onSurfaceChanged(GL10 gl, int width, int height) { + mSurfaceWidth = width; + mSurfaceHeight = height; + adjustVideoSize(); + Log.e("@@@", "onSurfaceChanged width: " + width + "...height.." + height); + glViewport(0, 0, width, height); + } + + @Override + public void onDrawFrame(GL10 gl) { + glClear(GLES30.GL_DEPTH_BUFFER_BIT | GLES30.GL_COLOR_BUFFER_BIT); + adjustVideoSize(); + glVertexAttribPointer(vertexPosition, POSITION_COMPONENT_COUNT, GLES30.GL_FLOAT, false, 0, mVertextBuffer); + glVertexAttribPointer(texturePosition, TEXTURE_COMPONENT_COUNT, GLES30.GL_FLOAT, false, 0, mTextureBuffer); + synchronized (this) { + if (mUpdateSurfaceTexture) { + mSurfaceTexture.updateTexImage(); + mUpdateSurfaceTexture = false; + } + } + GLES30.glEnableVertexAttribArray(vertexPosition); + GLES30.glEnableVertexAttribArray(texturePosition); + GLES30.glUniform1i(samplerTexturePosition, 0); + // 绘制 + GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4); + GLES30.glFlush(); + GLES30.glDisableVertexAttribArray(vertexPosition); + GLES30.glDisableVertexAttribArray(texturePosition); + } + + public void setVideoSize(int width, int height) { + if (mVideoWidth == width && mVideoHeight == height) { + return; + } + // videoWidth 272 + // videoHeight 480 + mVideoWidth = width; + mVideoHeight = height; + mNeedUpdateSize = true; + } + + private void adjustVideoSize() { + if (mVideoWidth == 0 || mVideoHeight == 0 || mSurfaceHeight == 0 || mSurfaceWidth == 0) { + return; + } + if (!mNeedUpdateSize) { + return; + } + mNeedUpdateSize = false; + float widthRation = (float) mSurfaceWidth / mVideoWidth; + float heightRation = (float) mSurfaceHeight / mVideoHeight; + float ration = Math.max(widthRation, heightRation); + // 把视频宽高最小的一个扩大到Surface的大小 + int targetVideoWidth = Math.round(mVideoWidth * ration); + int targetVideoHeight = Math.round(mVideoHeight * ration); + // 扩大之后的宽高除以目前surface的宽高,来算错各自要xy的比例,这俩里面有一个肯定是1 + + float rationX = (float) targetVideoWidth / mSurfaceWidth; + float rationY = (float) targetVideoHeight / mSurfaceHeight; + + float[] targetPositionData = new float[]{ + POINT_DATA[0] / rationY, POINT_DATA[1] / rationX, + POINT_DATA[2] / rationY, POINT_DATA[3] / rationX, + POINT_DATA[4] / rationY, POINT_DATA[5] / rationX, + POINT_DATA[6] / rationY, POINT_DATA[7] / rationX, + + }; + // 换算缩放后的顶点坐标。后面在onDraw()方法中会有这个值设置给顶点着色器 + mVertextBuffer.clear(); + mVertextBuffer.put(targetPositionData); + mVertextBuffer.position(0); + } + + private IVideoTextureRenderListener mTextureRenderListener; + + public void setIVideoTextureRenderListener(IVideoTextureRenderListener render) { + mTextureRenderListener = render; + } + + public interface IVideoTextureRenderListener { + void onCreate(SurfaceTexture surfaceTexture); + } +} +``` + + + +下面是具体的效果分别是填充宽和填充高的效果: + + + 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 4d0adb8f..77c83770 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" @@ -74,7 +74,7 @@ RGB三个通道的颜色取反,而alpha通道不变。 ## 视频播放滤镜实现 -滤镜的编写主要是靠基类。 +首先需要在GLSurfaceView.Renderer的实现类中提供一个setFilter()的方法,然后在onDrawFrame()的时候再去调用这个Filter的onDraw()方法,所以滤镜的编写主要是靠基类。 - 先抽取BaseFilter类,封装好每个滤镜的着色器及onDraw等方法。 - 不同的滤镜都继承该BaseFilter类,然后实现有区别的部分。其实每个不同滤镜的区别就是着色器的不同。 From 723968520c8d34f4e936cd7719969bba00bc40aa Mon Sep 17 00:00:00 2001 From: CharonChui Date: Sun, 29 Mar 2020 19:09:09 +0800 Subject: [PATCH 014/213] update --- .../11.OpenGL ES\346\273\244\351\225\234.md" | 281 +++++++++++++++++- 1 file changed, 279 insertions(+), 2 deletions(-) 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 77c83770..ed330195 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" @@ -90,9 +90,286 @@ RGB三个通道的颜色取反,而alpha通道不变。 - onDestroy():销毁,用于资源回收。 +```java +public class BaseFilter { + private static final int POSITION_COMPONENT_COUNT = 2; + private static final int TEXTURE_COMPONENT_COUNT = 2; + + protected int mProgram; + @RawRes + private int mVertexShaderResId; + @RawRes + private int mFragmentShaderResId; + + private boolean mInited; + + private int vertexPosition; + private int texturePosition; + private int samplerTexturePosition; + + //渲染线程 + private LinkedList mRunOnDraw = new LinkedList(); + + public BaseFilter() { + this(R.raw.video_no_filter_vertex_shader, R.raw.video_no_filter_fragment_shader); + } + + public BaseFilter(@RawRes int vertexShaderResId, @RawRes int fragmentShaderResId) { + mVertexShaderResId = vertexShaderResId; + mFragmentShaderResId = fragmentShaderResId; + } + + public void init() { + if (!mInited) { + onInit(); + mInited = true; + onInited(); + } + } + + public void onInit() { + handleProgram(MyApplication.getInstance(), mVertexShaderResId, mFragmentShaderResId); + vertexPosition = glGetAttribLocation("vPosition"); + texturePosition = glGetAttribLocation("vCoordPosition"); + samplerTexturePosition = glGetUniformLocation("uSamplerTexture"); + } + + /** + * readResource -> compileShader -> linkProgram -> useProgram + * + * @param context + * @param vertexShader + * @param fragmentShader + */ + protected void handleProgram(@NonNull Context context, @RawRes int vertexShader, @RawRes int fragmentShader) { + String vertexShaderStr = ResReadUtils.readResource(context, vertexShader); + int vertexShaderId = ShaderUtils.compileVertexShader(vertexShaderStr); + //编译片段着色程序 + String fragmentShaderStr = ResReadUtils.readResource(context, fragmentShader); + int fragmentShaderId = ShaderUtils.compileFragmentShader(fragmentShaderStr); + //连接程序 + mProgram = ShaderUtils.linkProgram(vertexShaderId, fragmentShaderId); + //在OpenGLES环境中使用程序 + GLES30.glUseProgram(mProgram); + } + + public void onInited() { + + } + + public final void destroy() { + mInited = false; + GLES30.glDeleteProgram(mProgram); + onDestroy(); + } + + public void onDestroy() { + + } + + public void onDraw(final int textureId, final FloatBuffer mVertextBuffer, + final FloatBuffer mTextureBuffer) { + runPendingOnDrawTasks(); + if (!mInited) { + return; + } + + glVertexAttribPointer(vertexPosition, POSITION_COMPONENT_COUNT, GLES30.GL_FLOAT, false, 0, mVertextBuffer); + glVertexAttribPointer(texturePosition, TEXTURE_COMPONENT_COUNT, GLES30.GL_FLOAT, false, 0, mTextureBuffer); + GLES30.glEnableVertexAttribArray(vertexPosition); + GLES30.glEnableVertexAttribArray(texturePosition); + GLES30.glUniform1i(samplerTexturePosition, 0); +// if (textureId != GL.NO_TEXTURE) { +// GLES20.glActiveTexture(GLES20.GL_TEXTURE0); +// GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId); +// GLES20.glUniform1i(glUniformTexture, 0); +// } + + onDrawArraysPre(); + GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4); + GLES30.glDisableVertexAttribArray(vertexPosition); + GLES30.glDisableVertexAttribArray(texturePosition); + } + + protected void runPendingOnDrawTasks() { + while (!mRunOnDraw.isEmpty()) { + mRunOnDraw.removeFirst().run(); + } + } + + + /** + * 设置着色器中对象float值 + */ + protected void setFloat(final int location, final float floatValue) { + runOnDraw(new Runnable() { + @Override + public void run() { + GLES30.glUniform1f(location, floatValue); + } + }); + } + + /** + * 设置着色器中对象组值float值 + */ + protected void setFloatVec2(final int location, final float[] arrayValue) { + runOnDraw(new Runnable() { + @Override + public void run() { + GLES30.glUniform2fv(location, 1, FloatBuffer.wrap(arrayValue)); + } + }); + } + + /** + * 设置着色中数组值 + */ + protected void setFloatVec3(final int location, final float[] arrayValue) { + runOnDraw(new Runnable() { + @Override + public void run() { + GLES20.glUniform3fv(location, 1, FloatBuffer.wrap(arrayValue)); + } + }); + } + + /** + * 设置着色器中对象组值float值 + */ + protected void setFloatVec4(final int location, final float[] arrayValue) { + runOnDraw(new Runnable() { + @Override + public void run() { + GLES20.glUniform4fv(location, 1, FloatBuffer.wrap(arrayValue)); + } + }); + } + + /** + * 设置着色器中3维矩阵的值 + */ + protected void setUniformMatrix3f(final int location, final float[] matrix) { + runOnDraw(new Runnable() { + @Override + public void run() { + GLES20.glUniformMatrix3fv(location, 1, false, matrix, 0); + } + }); + } + + /** + * 设置着色器中4维矩阵的值 + */ + protected void setUniformMatrix4f(final int location, final float[] matrix) { + runOnDraw(new Runnable() { + @Override + public void run() { + GLES20.glUniformMatrix4fv(location, 1, false, matrix, 0); + } + }); + } + + + protected void runOnDraw(Runnable runnable) { + synchronized (mRunOnDraw) { + mRunOnDraw.addLast(runnable); + } + } + + protected void onDrawArraysPre() { + + } + + + protected void glViewport(int x, int y, int width, int height) { + GLES30.glViewport(x, y, width, height); + } + + protected void glClearColor(float red, float green, float blue, float alpha) { + GLES30.glClearColor(red, green, blue, alpha); + } + + protected void glClear(int mask) { + GLES30.glClear(mask); + } + + protected static void glEnableVertexAttribArray(int index) { + GLES30.glEnableVertexAttribArray(index); + } + + protected void glDisableVertexAttribArray(int index) { + GLES30.glDisableVertexAttribArray(index); + } + + protected int glGetAttribLocation(String name) { + return GLES30.glGetAttribLocation(mProgram, name); + } + + protected int glGetUniformLocation(String name) { + return GLES30.glGetUniformLocation(mProgram, name); + } + + protected void glUniformMatrix4fv(int location, int count, boolean transpose, float[] value, int offset) { + GLES30.glUniformMatrix4fv(location, count, transpose, value, offset); + } + + protected void glDrawArrays(int mode, int first, int count) { + GLES30.glDrawArrays(mode, first, count); + } + + protected void glDrawElements(int mode, int count, int type, java.nio.Buffer indices) { + GLES30.glDrawElements(mode, count, type, indices); + } + + protected void orthoM(String name, int width, int height) { + ProjectionMatrixUtil.orthoM(mProgram, width, height, name); + } + + protected void glVertexAttribPointer( + int indx, + int size, + int type, + boolean normalized, + int stride, + java.nio.Buffer ptr) { + GLES30.glVertexAttribPointer(indx, size, type, normalized, stride, ptr); + } + + protected void glActiveTexture(int texture) { + GLES30.glActiveTexture(GLES30.GL_TEXTURE0); + } + + protected void glBindTexture(int target, int texture) { + GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, texture); + } + + protected void glUniform1i(int location, int x) { + GLES20.glUniform1i(location, x); + } + + public int getProgram() { + return mProgram; + } +} +``` - - +在Render中改变的地方就是onDrawFrame()方法中去调用Filter.onDraw()方法: +```java + @Override + public void onDrawFrame(GL10 gl) { + glClear(GLES30.GL_DEPTH_BUFFER_BIT | GLES30.GL_COLOR_BUFFER_BIT); + adjustVideoSize(); + synchronized (this) { + if (mUpdateSurfaceTexture) { + mSurfaceTexture.updateTexImage(); + mUpdateSurfaceTexture = false; + } + } + runAll(mRunOnDraw); + mFilter.onDraw(mTextureId, mVertextBuffer, mTextureBuffer); + } +``` From b898c6fc6b67b4cc2f353c577f217afa607691a7 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 3 Apr 2020 21:39:46 +0800 Subject: [PATCH 015/213] update --- VideoDevelopment/DASH.md | 96 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 VideoDevelopment/DASH.md diff --git a/VideoDevelopment/DASH.md b/VideoDevelopment/DASH.md new file mode 100644 index 00000000..87f9790a --- /dev/null +++ b/VideoDevelopment/DASH.md @@ -0,0 +1,96 @@ +# HLS协议 + +HLS(HTTP Live Streaming)协议:是苹果公司实现的基于HTTP的流媒体传输协议,可实现流媒体的直播和点播。HLS点播,基本上就是常见的分段HTTP点播,不同在于,它的分段非常小。相对于常见的流媒体直播协议,例如RTMP协议、RTSP协议、MMS协议等,HLS直播最大的不同在于,直播客户端获取到的,并不是一个完整的数据流。HLS协议在服务器端将直播数据流存储为连续的、很短时长的媒体文件(MPEG-TS格式),而客户端则不断的下载并播放这些小文件。因为服务器端总是会将最新的直播数据生成新的小文件,这样客户端只要不停的按顺序播放从服务器获取到的文件,就实现了直播。由此可见,基本上可以认为,HLS是以点播的技术方式来实现直播。由于数据通过HTTP协议传输,所以完全不用考虑防火墙或者代理的问题,而且分段文件的时长很短,客户端可以很快的选择和切换码率,以适应不同带宽条件下的播放。不过HLS的这种技术特点,决定了它的延迟一般总是会高于普通的流媒体直播协议。它也解决了RTMP协议存在的一些问题,例如RTMP协议不使用标准的HTTP接口传输数据(TCP、UDP端口),所以在一些特殊的网络环境下可能被防火墙屏蔽掉,而HLS使用的是HTTP协议传输数据(80端口),不会遇到被防火墙屏蔽的情况。 + +在开始一个流媒体会话时,客户端会下载一个包含元数据的extended M3U(m3u8) playlist文件,用于寻找可用的媒体流(将视频分为一个个视频小分片,然后用m3u8索引表进行管理,由于客户端下载到的视频都是5-10秒的完整数据,所以视频的流畅性很好,但是同样也引入了很大的延迟(一般延迟在10-30s左右))。 + +上面提到了m3u8,那m3u8构成是?直播中m3u8、ts如何实时更新? + +- 是一个索引地址/播放列表,通过FFmpeg将本地的xxx.mp4进行切片处理,生成m3u8播放列表(索引文件)和N多个 .ts文件,并将其(m3u8、N个ts)放置在本地搭建好的webServer服务器的指定目录下,我就可以得到一个可以实时播放的网址,我们把这个m3u8地址复制到 VLC 上就可以实时观看! 在 HLS 流下,本地视频被分割成一个一个的小切片,一般10秒一个,这些个小切片被 m3u8管理,并且随着终端的FFmpeg 向本地拉流的命令而实时更新,影片进度随着拉流的进度而更新,播放过的片段不在本地保存,自动删除,直到该文件播放完毕或停止,ts 切片会相应的被删除,流停止,影片不会立即停止,影片播放会滞后于拉流一段时间 + +HLS的整体流程为: + +视频源文件 -> 服务器(编码器、流分割器) -> 分发器(索引文件、*.ts) -> 传输网络 -> 客户端 + + + +[http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8](https://link.jianshu.com?t=http%3A%2F%2Fwww.modrails.com%2Fvideos%2Fpassenger_nginx.mov) + +``` +#EXTM3U // m3u8文件头 +#EXT-X-VERSION:3 // 协议版本 +#EXT-X-MEDIA-SEQUENCE:304240 // 第一个ts文件的序列号 +#EXT-X-TARGETDURATION:10 // 每个ts文件的最大时长 +#EXTINF:10.000, // 每个ts文件的持续时间 +cctv1hd-1585920024000.ts // ts文件的url +#EXTINF:10.000, +cctv1hd-1585920034000.ts +#EXTINF:10.000, +cctv1hd-1585920044000.ts +#EXTINF:10.000, +cctv1hd-1585920054000.ts +#EXTINF:10.000, +cctv1hd-1585920064000.ts +#EXTINF:10.000, +cctv1hd-1585920074000.ts +``` + + + +# 简介 + + + +DASH(MPEG-DASH)全称为Dynamic Adaptive Streaming over HTTP。是国际标准组MPEG 2014年推出的技术标准,主要目标是形成IP网络承载单一格式的流媒体并提供高效与高质量服务的统一方案,解决多制式传输方案(HTTP Live Streaming, Microsoft Smooth Streaming, HTTP Dynamic Streaming)并存格局下的存储与服务能力浪费、运营高成本与复杂度、系统间互操作弱等问题。 + +DASH是基于HTTP的动态自适应的比特率流技术,使用的传输协议是TCP(有些老的客户端直播会采用UDP协议直播, 例如YY, 齐齐视频等). 和HLS, HDS技术类似, 都是把视频分割成一小段一小段, 通过HTTP协议进行传输,客户端得到之后进行播放;不同的是MPEG-DASH支持MPEG-2 TS、MP4等多种格式, 可以将视频按照多种编码切割, 下载下来的媒体格式既可以是ts文件也可以是mp4文件, 所以当客户端加载视频时, 按照当前的网速和支持的编码加载相应的视频片段进行播放. + +因为是分片的,客户端可以自由选择需要播放的媒体分片,可以实现`Adaptive Bitrate Streaming`技术,不同画质内容无缝切换,提供更好的播放体验。 + +YouTube采用DASH!其网页端及移动端APP都使用了DASH。 + +DASH的整个流程: + +内容生成服务器(编码模块、封装模块) -> 流媒体服务器(MPD、媒体文件、HTTP服务器) <-> DASH客户端(控制引擎、媒体引擎、HTTP接入容器) + + + +在2012年由ISO/IEC发表,正式成为国际标准。该技术与编解码器无关,可使用H.265,H.264,VP9等任何编码器进行编码。 + +DASH和苹果的HLS(HTTP Live Streaming)技术相似,通过把内容分割成小的基于HTTP的文件段序列,来进行流媒体播放。各个文件段可以设置成不同的比特率进行编码,以满足不同客户端的网络需求。例如,DASH客户端可以根据当前的网络状况,自动选择对应的最匹配的比特率文件段下载,进行回访,而不会引起停顿或重新缓冲。这样DASH可以做到无缝的适应不断变化的网络条件,并提供高品质的播放,而能够尽量减少播放的停顿或缓冲。 + + + +国外主要是YouTube、BBC、ITV等都已经开始推行DASH,国内B站、爱奇艺、腾讯 + +# DASH vs HLS + + + + + +# DASH + +- MPD +- + + + + + + + + + + + + + + + + + + + + + From a8f73ad225055abe8df89a8b2c5746e6afac81d7 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 6 Apr 2020 19:18:11 +0800 Subject: [PATCH 016/213] add notes --- VideoDevelopment/DASH.md | 96 ---------- .../DASH.md" | 158 +++++++++++++++++ .../HLS.md" | 165 ++++++++++++++++++ ...32\344\277\241\345\215\217\350\256\256.md" | 0 4 files changed, 323 insertions(+), 96 deletions(-) delete mode 100644 VideoDevelopment/DASH.md create mode 100644 "VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/DASH.md" create mode 100644 "VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/HLS.md" rename "VideoDevelopment/\346\265\201\345\252\222\344\275\223\351\200\232\344\277\241\345\215\217\350\256\256.md" => "VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/\346\265\201\345\252\222\344\275\223\351\200\232\344\277\241\345\215\217\350\256\256.md" (100%) diff --git a/VideoDevelopment/DASH.md b/VideoDevelopment/DASH.md deleted file mode 100644 index 87f9790a..00000000 --- a/VideoDevelopment/DASH.md +++ /dev/null @@ -1,96 +0,0 @@ -# HLS协议 - -HLS(HTTP Live Streaming)协议:是苹果公司实现的基于HTTP的流媒体传输协议,可实现流媒体的直播和点播。HLS点播,基本上就是常见的分段HTTP点播,不同在于,它的分段非常小。相对于常见的流媒体直播协议,例如RTMP协议、RTSP协议、MMS协议等,HLS直播最大的不同在于,直播客户端获取到的,并不是一个完整的数据流。HLS协议在服务器端将直播数据流存储为连续的、很短时长的媒体文件(MPEG-TS格式),而客户端则不断的下载并播放这些小文件。因为服务器端总是会将最新的直播数据生成新的小文件,这样客户端只要不停的按顺序播放从服务器获取到的文件,就实现了直播。由此可见,基本上可以认为,HLS是以点播的技术方式来实现直播。由于数据通过HTTP协议传输,所以完全不用考虑防火墙或者代理的问题,而且分段文件的时长很短,客户端可以很快的选择和切换码率,以适应不同带宽条件下的播放。不过HLS的这种技术特点,决定了它的延迟一般总是会高于普通的流媒体直播协议。它也解决了RTMP协议存在的一些问题,例如RTMP协议不使用标准的HTTP接口传输数据(TCP、UDP端口),所以在一些特殊的网络环境下可能被防火墙屏蔽掉,而HLS使用的是HTTP协议传输数据(80端口),不会遇到被防火墙屏蔽的情况。 - -在开始一个流媒体会话时,客户端会下载一个包含元数据的extended M3U(m3u8) playlist文件,用于寻找可用的媒体流(将视频分为一个个视频小分片,然后用m3u8索引表进行管理,由于客户端下载到的视频都是5-10秒的完整数据,所以视频的流畅性很好,但是同样也引入了很大的延迟(一般延迟在10-30s左右))。 - -上面提到了m3u8,那m3u8构成是?直播中m3u8、ts如何实时更新? - -- 是一个索引地址/播放列表,通过FFmpeg将本地的xxx.mp4进行切片处理,生成m3u8播放列表(索引文件)和N多个 .ts文件,并将其(m3u8、N个ts)放置在本地搭建好的webServer服务器的指定目录下,我就可以得到一个可以实时播放的网址,我们把这个m3u8地址复制到 VLC 上就可以实时观看! 在 HLS 流下,本地视频被分割成一个一个的小切片,一般10秒一个,这些个小切片被 m3u8管理,并且随着终端的FFmpeg 向本地拉流的命令而实时更新,影片进度随着拉流的进度而更新,播放过的片段不在本地保存,自动删除,直到该文件播放完毕或停止,ts 切片会相应的被删除,流停止,影片不会立即停止,影片播放会滞后于拉流一段时间 - -HLS的整体流程为: - -视频源文件 -> 服务器(编码器、流分割器) -> 分发器(索引文件、*.ts) -> 传输网络 -> 客户端 - - - -[http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8](https://link.jianshu.com?t=http%3A%2F%2Fwww.modrails.com%2Fvideos%2Fpassenger_nginx.mov) - -``` -#EXTM3U // m3u8文件头 -#EXT-X-VERSION:3 // 协议版本 -#EXT-X-MEDIA-SEQUENCE:304240 // 第一个ts文件的序列号 -#EXT-X-TARGETDURATION:10 // 每个ts文件的最大时长 -#EXTINF:10.000, // 每个ts文件的持续时间 -cctv1hd-1585920024000.ts // ts文件的url -#EXTINF:10.000, -cctv1hd-1585920034000.ts -#EXTINF:10.000, -cctv1hd-1585920044000.ts -#EXTINF:10.000, -cctv1hd-1585920054000.ts -#EXTINF:10.000, -cctv1hd-1585920064000.ts -#EXTINF:10.000, -cctv1hd-1585920074000.ts -``` - - - -# 简介 - - - -DASH(MPEG-DASH)全称为Dynamic Adaptive Streaming over HTTP。是国际标准组MPEG 2014年推出的技术标准,主要目标是形成IP网络承载单一格式的流媒体并提供高效与高质量服务的统一方案,解决多制式传输方案(HTTP Live Streaming, Microsoft Smooth Streaming, HTTP Dynamic Streaming)并存格局下的存储与服务能力浪费、运营高成本与复杂度、系统间互操作弱等问题。 - -DASH是基于HTTP的动态自适应的比特率流技术,使用的传输协议是TCP(有些老的客户端直播会采用UDP协议直播, 例如YY, 齐齐视频等). 和HLS, HDS技术类似, 都是把视频分割成一小段一小段, 通过HTTP协议进行传输,客户端得到之后进行播放;不同的是MPEG-DASH支持MPEG-2 TS、MP4等多种格式, 可以将视频按照多种编码切割, 下载下来的媒体格式既可以是ts文件也可以是mp4文件, 所以当客户端加载视频时, 按照当前的网速和支持的编码加载相应的视频片段进行播放. - -因为是分片的,客户端可以自由选择需要播放的媒体分片,可以实现`Adaptive Bitrate Streaming`技术,不同画质内容无缝切换,提供更好的播放体验。 - -YouTube采用DASH!其网页端及移动端APP都使用了DASH。 - -DASH的整个流程: - -内容生成服务器(编码模块、封装模块) -> 流媒体服务器(MPD、媒体文件、HTTP服务器) <-> DASH客户端(控制引擎、媒体引擎、HTTP接入容器) - - - -在2012年由ISO/IEC发表,正式成为国际标准。该技术与编解码器无关,可使用H.265,H.264,VP9等任何编码器进行编码。 - -DASH和苹果的HLS(HTTP Live Streaming)技术相似,通过把内容分割成小的基于HTTP的文件段序列,来进行流媒体播放。各个文件段可以设置成不同的比特率进行编码,以满足不同客户端的网络需求。例如,DASH客户端可以根据当前的网络状况,自动选择对应的最匹配的比特率文件段下载,进行回访,而不会引起停顿或重新缓冲。这样DASH可以做到无缝的适应不断变化的网络条件,并提供高品质的播放,而能够尽量减少播放的停顿或缓冲。 - - - -国外主要是YouTube、BBC、ITV等都已经开始推行DASH,国内B站、爱奇艺、腾讯 - -# DASH vs HLS - - - - - -# DASH - -- MPD -- - - - - - - - - - - - - - - - - - - - - - diff --git "a/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/DASH.md" "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/DASH.md" new file mode 100644 index 00000000..bb951503 --- /dev/null +++ "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/DASH.md" @@ -0,0 +1,158 @@ +# 简介 + +除了前面将的Apple的HLS,还有Adobe HTTP Dynamic Streaming (HDS)、Microsoft Smooth Streaming (MSS)。他们各家的协议原理大致相同,但是格式又不一样,也无法兼容,所以Moving Picture Expert Group (MPEG) 就把大家叫到了一起,呼吁大家一起来制定一个标准的,然后就有了[MPEG-DASH](https://www.encoding.com/mpeg-dash/),它的主要目标是形成IP网络承载单一格式的流媒体并提供高效与高质量服务的统一方案,解决多制式传输方案(HTTP Live Streaming, Microsoft Smooth Streaming, HTTP Dynamic Streaming)并存格局下的存储与服务能力浪费、运营高成本与复杂度、系统间互操作弱等问题。 + +[DASH(MPEG-DASH)](https://mpeg.chiariglione.org/standards/mpeg-dash/)全称为Dynamic Adaptive Streaming over HTTP.是由MPEG和ISO批准的独立于供应商的国际标准,它是一种基于HTTP的使用TCP传输协议的流媒体传输技术。MPEG-DASH是一种自适应比特率流技术,可根据实时网络状况实现动态自适应下载。和HLS, HDS技术类似, 都是把视频分割成一小段一小段, 通过HTTP协议进行传输,客户端得到之后进行播放;不同的是MPEG-DASH支持MPEG-2 TS、MP4(最新的HLS也支持了MP4)等多种格式, 可以将视频按照多种编码切割, 下载下来的媒体格式既可以是ts文件也可以是mp4文件,MPEG—DASH技术与编解码器无关,可使用H.265,H.264,VP9等任何编解码器进行编码。 + +安卓平台上的ExoPlayer支持MPEG-DASH。另外,三星、索尼、飞利浦、松下的一些较新型号的智能电视支持MPEG—DASH。Google的Chromecast、YouTube 和Netflix 也已支持MPEG-DASH。 + +![dash_compare](https://raw.githubusercontent.com/CharonChui/Pictures/master/dash_compare.png) + +## 思想 + +![dash_main_idea](https://raw.githubusercontent.com/CharonChui/Pictures/master/dash_main_idea.png) + +DASH的核心思想是,在服务端,视频被提前编好多种码率,并且被切成固定长度的视频片段,存放到HTTP服务器中,当客户端播放时,通过HTTP请求向服务器请求视频切片,并根据网络状况的变化,请求相应质量的视频切片,从而达到对网络带宽的最大利用,并且保证播放流畅。可以实现不同画质内容无缝切换。所以在 YouTube 切换画质时完全不会黑屏,更不会影响观看。 + +![image-20200406163508642](https://raw.githubusercontent.com/CharonChui/Pictures/master/a_dash_scenario.png) + + + +DASH的整个流程: + +内容生成服务器(编码模块、封装模块) -> 流媒体服务器(MPD、媒体文件、HTTP服务器) <-> DASH客户端(控制引擎、媒体引擎、HTTP接入容器) + +![image-20200406164238038](https://raw.githubusercontent.com/CharonChui/Pictures/master/mpd_hierarchical_data.png) + +### MPD + +DASH采用3GPP AHS中定义的MPD(Media Presentation Description)作为媒体文件的描述文件(manifest),作用类似HLS的m3u8文件。MPD文件以XML格式组织,用于描述segment的信息,比如时间、url、视频分辨率、码率等。 +DASH会通过media presentation description (MPD)将视频内容切片成一个很短的文件片段,每个切片都有多个不同的码率,DASH Client可以根据网络的情况选择一个码率进行播放,支持在不同码率之间无缝切换。 +DASH中的重要概念 + +### Period + +MPD文件包含一个或者多个片段(Period),它表示时间轴上的一段时间,每个片段都有一个起始时间和结束时间,并且包含了一个或者多个适配集合(Adaptation Set)。每个适配集合提供了一个或者多个媒体组件的信息,并包含了多种不同的码率。每个适配集合又是由多个呈现(Representation)组成,每个呈现就是同一个视频的不同特征的版本,如码率、分辨率等特征。由于每个的视频都要被切成固定长度的切片,因此每个呈现包括多个视频切片(Segment),每个视频切片都有一个URL地址,这样客户端就可以通过这个地址向服务器发送HTTP GET请求获取该片段。同一个Period内,意味着可用的媒体内容及其各个可用码率(Representation)不会发生变更。直播情况下,“可能”需要周期地去服务器更新MPD文件,服务器可能会移除旧的已经过时的Period,或是添加新的Period。新的Period中可能会添加新的可用码率或去掉上一个Period中存在的某些码率(Representation)。 + +### Adaptation Set + + 一个Period由一个或者多个Adaptationset组成。Adaptationset由一组可供切换的不同码率的码流(Representation)组成,这些码流中可能包含一个(ISO profile)或者多个(TS profile)media content components,因为ISO profile的mp4或者fmp4 segment中通常只含有一个视频或者音频内容,而TS profile中的TS segment同时含有视频和音频内容,当同时含有多个media component content时,每个被复用的media content component将被单独描述 + +### Representation + +每个Adaptationset包含了一个或者多个Representations,一个Representation包含一个或者多个media streams,每个media stream对应一个media content component。为了适应不同的网络带宽,dash客户端可能会从一个Representation切换到另外一个Representation,如果不支持某个Representation的编码格式,在切换时可以忽略之。 + +### Segments + +Segments可以包含任何媒体数据,关于容器,官方提供了两种建议: ISO base media file format(比如MP4文件格式)和MPEG-2 Transport Stream。 + +每个Representation会划分为多个Segment。Segment分为4类,其中,最重要的是:Initialization Segment(每个Representation都包含1个Init Seg),Media Segment(每个Representation的媒体内容包含若干Media Seg) + +- Initialization Segment: + + Representation的Segments一般都采用1个Init Segment+多个普通Segment的方式,还有一种形式就是Self Initialize Segment,这种形式没有单独的Init Segment,初始化信息包括在了各个Segment中。Init Segment中包含了解封装需要的全部信息,比如Representation中有哪些音视频流,各自的编码格式及参数。对于 ISO profile来说(容器为MP4),包含了moov box,H264的sps/pps数据等关键信息存放于此(avCc box)。另外,同一个Adaptation set的多个Representation还可能共享同一个Init Segment,该种情况下,对于ISO profile来说,诸如stsd box,avCc box等重要的box会含有多个entry,每个entry对应一个Representation,第一个entry对应第一个Representation,第二个entry对应第二个Representation,以此类推。 + +- Subsegment + + Segment可能进一步划分为subsegment,每个subsegment由数个Acess Unit组成,Segment index提供了subsegment相对于Segment的字节范围和presentation time range 。客户端可以先下载Segment index。 + +### SAP和无缝切换以及SEEK + + SAP:Stream Acess Point,可以简单理解为I帧,每个Segment的第一个帧都是SAP,因此Seek时可直接Seek到某一个Segment的起始位置,利用Init Segment+Seek到的某个Segment的数据,在解封装后可实现完美解码。一般来说,同一个Adaptation set中的多个Representation是Segment Align的(当Adaptation set的属性@segmentAlignment不为false时),因此,当从Representation A切换到Representation B时,如果当前Representation A的第N个Segment已经下载完成,切换时直接下载Representation B的第N+1个Segment即可。 + + + + + +![dash_compare2.png](https://raw.githubusercontent.com/CharonChui/Pictures/master/dash_compare2.png) + +码率自适应切换算法 + +- 基于带宽的码率自适应切换算法 +- 基于缓存的码率自适应切换算法 + +![image-20200406165443536](https://raw.githubusercontent.com/CharonChui/Pictures/master/dash_change_compare.png) + +此基于时间缓存的码率自适应算法对网络带宽变化反应敏感,能够有效的提高平均码率,但同时码率切换次数过大,尤其是在网络状况波动很大的情况下,这势必会造成用户体验的下降。 + +- MSS拥有最高的平均码率和较少的切换次数 + +- HLS的切换次数最少,但是以最低的平均码率作为代价 + +- HDS不能保证流程的播放 + +- DASH有足够的竞争力,也具有巨大的提升空间。 + + + +## DASH地址 + +http://ftp.itec.aau.at/datasets/mmsys12/BigBuckBunny/MPDs/BigBuckBunnyNonSeg_2s_isoffmain_DIS_23009_1_v_2_1c2_2011_08_30.mpd + +http://www-itec.uni-klu.ac.at/ftp/datasets/mmsys12/BigBuckBunny/bunny_2s/bunny_2s_50kbit/bunny_50kbit_dashNonSeg.mp4 + + + +## fMP4 + +fMP4(fragmented MP4),可以简单理解为分片化的MP4,是DASH采用的媒体文件格式,文件扩展名通常为(.m4s或直接用.mp4)。 + +![fMP4](https://img-blog.csdn.net/20171107114807709?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveXVlX2h1YW5n/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) + + + +### fMP4与ts的区别 + +### 媒体数据与元数据的分离 + +在mp4格式中,元数据可以和媒体数据很好地分开存储,后者都在mdat box中,而在ts中,诸多es流和header/metadata信息是复用在一起的。 + 元数据的分离允许我们在streaming中先读取各个流的元数据,知道他们的媒体内容的属性(比如不同的视频质量、不同的语言等),从而可以更好地在不同的media data之间做自适应切换。 + 当然,在更实际的应用场景中,比如在dash协议中会直接把这些元数据信息写在mpd中,player可以只读一个mpd就知道各个媒体数据的属性 + +### 各个Track独立存储 + +在fmp4中,不仅媒体数据和metadata相互独立的存储,音视频track的数据也可以分开存储,这里的“分开”已经不仅仅局限于box层面的分开,而是真的可以分开存储于不同的目录。在这种情况下,player只需要读一个记录了它们各自存储位置的manifest,即可去对应的位置download它们的分片,只要做好音视频分片之间的同步工作,就可以正常的播放。 + 举个例子,下面是一个dash中常见的mpd: + +在这个mpd中我们看到视频的分片都是存储在video/1/这个目录下,而音频分片都存储在audio/und/mp4a/1这个目录下,而player还是可以将它们拼接到一起完成播放。 + 相对地,在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)文件可以更加简洁,如下: + +针对不同语言的音频,都只需要存一个大文件就够了。 +相对地,在streaming TS流时,不得不在manifest(m3u8)文件中把成百上千个ts分片文件全都老老实实地记录下来。 + +所以Dash同样可以支持下面的操作: + +- 音频视频分离,在后台播放时可以只拉取音频 +- 支持多音轨,多视频轨,多字幕任意切换 + +### 服务器的cache效率会降低 + 实际的streaming应用场景中,往往需要cdn的支持,经常会被client请求的媒体分片就会存在距离client最近的edge server上。对于fmp4 streaming的情况,因为需要的文件更少,cache命中率也就更高,举个例子:可能某一个audio track会和其他各种video track组合,那么就可以将这个audio track放在edge server上,而不用每次都跟origin server去请求。 + 相对地,在streaming TS流时,因为每一个音视频组合的都需要以复用文件的形式存储,组合数又非常多,相当于分母大了,edge server就会有很大的几率没有缓存需要的组合而要去向orgin server请求。 + +### 对Trick-play的支持 + +所谓Trick-play,就是快进、快退、直接跳到章节起点、慢动作播放这些“花式”播放功能。支持这些功能往往意味着要快速找到播放流中的关键帧,以快进播放为例,如果利用fmp4格式的特点,可以通过只读取每个媒体分片的moof加上mdat的起始(包含了关键帧图像)部分即可,说白了就是通过只显示关键帧的方法达到“快进”的视觉效果。因为fmp4格式中可以保证每一个分片一定是以IDR帧开始的,这就使得上述的方案实现起来非常方便。 + 相对地,在streaming TS流时,没有办法保证关键帧一定在什么位置,所以你可能需要解析一大堆TS packets才能找到关键帧的位置。 + +### 无缝码流切换 + +无缝码流切换实现的关键在于:当第一个码流播放结束时,也就是发生切换的时间,第二个码流一定要以关键帧开始播放。在streaming TS流时,因为不能保证每一个TS chunk一定以关键帧开始,做码流切换时就意味着要同时download两个码流的相应分片,同时解析两个码流,然后找到关键帧对应的位置,才能切换。同时下载、解析两个码流的媒体内容对网络带宽以及设备性能都形成了挑战。而且有意思的是,如果当前网络环境不佳,player想要切换到低码率码流,结果还要在本来就不好的网络环境下同时进行两个码流的下载,可谓是雪上加霜。 + 而在fmp4中,除了保证各个分片一定以IDR帧开始外,还能保证不同码流的分片之间在时间线上是对齐的。而且streaming fmp4流时因为不要求音视频复用存储,也就意味着视频和音频的同步点可以不一样,视频可以全都以GOP边界作为同步点,音频可以都以sync frame作为同步点,这都使得无缝码流切换更简单。 + +### 与DRM的集成 + +所谓DRM即数字版权管理,说白了就是对流进行加密,这东西在国内用的不多,但是在国外可是每一个内容提供商必须要有的东西。和编码标准一样,业界也存在很多DRM方案,为了避免每采用一个新的加密方案就要重新编一个码流,MPEG推出了通用加密(CENC)标准(23001-7 - Common Encryption)。使用这一标准的码流,就可以将一个码流应用于各种不同的DRM方案。在DASH spec中,也定义了Content Protection字段来对应这种加密方案。 + CENC使用的就是fMP4格式,这是利用了fMP4中音视频可以不复用同时还能提供独立于media data存储的metadata的特点。TS流就享受不了这样的好处了。 + +DASH和HLS之间的另一个关键区别是它支持DRM。可是,在DASH中不存在一个单一通用的DRM解决方案。例如,Google的Chrome支持Widevine,而Microsoft的Internet Explorer支持PlayReady。然而,通过使用MPEG-CENC(MPEG通用加密)结合加密媒体扩展(EME),视频流内容可以仅被加密一次。HLS支持AES-128加密,以及苹果自己的DRM,Fairplay。 + + + + + + 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" new file mode 100644 index 00000000..62acc949 --- /dev/null +++ "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/HLS.md" @@ -0,0 +1,165 @@ +# HLS协议 + +[HLS(HTTP Live Streaming)协议](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/StreamingMediaGuide/Introduction/Introduction.html):2009年由苹果公司实现的基于HTTP的流媒体传输协议,可实现流媒体的直播和点播。它的工作原理是把整个流分成一个个小的基于HTTP的文件来下载,每次只下载一些。 + +HLS协议规定: + +- 视频的封装格式是TS +- 视频的编码格式为H264,音频编码格式为MP3、AAC或者AC-3 +- 除了TS视频文件本身,还定义了用来控制播放的m3u8文件(文本文件) + +HLS协议由三部分组成: + +- HTTP:传输协议 +- m3u8:索引文件 +- TS:音视频媒体信息 + +HLS点播,基本上就是常见的分段HTTP点播,不同在于,它的分段非常小。相对于常见的流媒体直播协议,例如RTMP协议、RTSP协议、MMS协议等,HLS直播最大的不同在于,直播客户端获取到的,并不是一个完整的数据流。HLS协议在服务器端将直播数据流存储为连续的、很短时长的媒体文件(MPEG-TS格式),而客户端则不断的下载并播放这些小文件。因为服务器端总是会将最新的直播数据生成新的小文件,这样客户端只要不停的按顺序播放从服务器获取到的文件,就实现了直播。由此可见,基本上可以认为,HLS是以点播的技术方式来实现直播。由于数据通过HTTP协议传输,所以完全不用考虑防火墙或者代理的问题,而且分段文件的时长很短,客户端可以很快的选择和切换码率,以适应不同带宽条件下的播放。不过HLS的这种技术特点,决定了它的延迟一般总是会高于普通的流媒体直播协议。它也解决了RTMP协议存在的一些问题,例如RTMP协议不使用标准的HTTP接口传输数据(TCP、UDP端口),所以在一些特殊的网络环境下可能被防火墙屏蔽掉,而HLS使用的是HTTP协议传输数据(80端口),不会遇到被防火墙屏蔽的情况。 + +## TS + +TS(Transport Stream),全程为MPEG2-TS。它的特点就是要求从视频流的任一片段开始都是可以独立解码的。DVD节目中的MPEG2格式,确切的说是MPEG2-PS,全程是Program Stream,而TS的全称是Transport Stream。MPEG2-PS主要应用于存储的具有固定市场的节目,如DVD电影,而MPEG2-TS则主要应用于实时传送的节目,比如实时广播的电视节目。这两种格式的主要区别是什么呢?简单地打个比喻说,你将DVD上的[VOB](https://baike.baidu.com/item/VOB)文件的前面一截cut掉(或者干脆就是数据损坏),那么就会导致整个文件无法解码了,而电视节目是你任何时候打开电视机都能解码(收看)的,所以,MPEG2-TS格式的特点就是要求从[视频流](https://baike.baidu.com/item/视频流)的任一片段开始都是可以独立解码的。 + + + +在开始一个流媒体会话时,客户端会下载一个包含元数据的extended M3U(m3u8) playlist文件,用于寻找可用的媒体流(将视频分为一个个视频小分片,然后用m3u8索引表进行管理,由于客户端下载到的视频都是5-10秒的完整数据,所以视频的流畅性很好,但是同样也引入了很大的延迟(一般延迟在10-30s左右))。 + +- 是一个索引地址/播放列表,通过FFmpeg将本地的xxx.mp4进行切片处理,生成m3u8播放列表(索引文件)和N多个 .ts文件,并将其(m3u8、N个ts)放置在本地搭建好的webServer服务器的指定目录下,我就可以得到一个可以实时播放的网址,我们把这个m3u8地址复制到 VLC 上就可以实时观看! 在 HLS 流下,本地视频被分割成一个一个的小切片,一般10秒一个,这些个小切片被 m3u8管理,并且随着终端的FFmpeg 向本地拉流的命令而实时更新,影片进度随着拉流的进度而更新,播放过的片段不在本地保存,自动删除,直到该文件播放完毕或停止,ts 切片会相应的被删除,流停止,影片不会立即停止,影片播放会滞后于拉流一段时间 + + + + +- Media encoder(媒体编码) + +媒体编码器获取到音视频设备的实时信号,将其编码后压缩用于传输 + +- Stream segmenter(流切片器) + + 通常是一个软件,将流进行切片,切片的同时会创建一个索引文件(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视频流了。 + +### 多码率适配流(Master Playlist) + + + +客户端播放HLS视频流的逻辑其实非常简单,先下载一级Index file,它里面记录了二级索引文件(Alternate-A、Alternate-B、Alternate-C)的地址,然后客户端再去下载二级索引文件,二级索引文件中又记录了TS文件的下载地址,这样客户端就可以按顺序下载TS视频文件并连续播放。 + +#### 一级index文件 + +```bash +#EXTM3U // m3u8文件头 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1064000 +1000kbps.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=564000 +500kbps.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=282000 +250kbps.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2128000 +2000kbps.m3u8 +``` + +bandwidth指定视频流的比特率,PROGRAM-ID无用无需关注,每一个#EXT-X-STREAM-INF的下一行是二级index文件的路径,可以用相对路径也可以用绝对路径。例子中用的是相对路径。这个文件中记录了不同比特率视频流的二级index文件路径,客户端可以自己判断自己的现行网络带宽,来决定播放哪一个视频流。也可以在网络带宽变化的时候平滑切换到和带宽匹配的视频流。客户端会默认选择码率最高的情况,如果发现码率达不到,会请求降低码率的流。 + +```css +#EXTM3U +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-TARGETDURATION:10 // 指定当前视频流中的单个切片(ts)文件的最大时长(秒) +#EXTINF:10, // 指定每个媒体段ts的持续时间 +2000kbps-00001.ts +#EXTINF:10, +2000kbps-00002.ts +#EXTINF:10, +2000kbps-00003.ts +#EXTINF:10, +2000kbps-00004.ts +#EXTINF:10, + +... ... + +#EXTINF:10, +2000kbps-00096.ts +#EXTINF:10, +2000kbps-00097.ts +#EXTINF:10, +2000kbps-00098.ts +#EXTINF:10, +2000kbps-00099.ts +#EXTINF:10, +2000kbps-00100.ts +#ZEN-TOTAL-DURATION:999.66667 +#ZEN-AVERAGE-BANDWIDTH:2190954 +#ZEN-MAXIMUM-BANDWIDTH:3536205 +#EXT-X-ENDLIST +``` + +二级文件实际负责给出ts文件的下载地址,这里同样使用了相对路径。#EXTINF表示每个ts切片视频文件的时长。#EXT-X-TARGETDURATION指定当前视频流中的切片文件的最大时长,也就是说这些ts切片的时长不能大于#EXT-X-TARGETDURATION的值。#EXT-X-PLAYLIST-TYPE:VOD的意思是当前的视频流并不是一个直播流,而是点播流,换句话说就是该视频的全部的ts文件已经被生成好了,#EXT-X-ENDLIST这个表示视频结束,有这个标志同时也说明当前的流是一个非直播流。 + +#### 播放模式 + +- **点播VOD**的特点就是当前时间点可以获取到所有index文件和ts文件,二级index文件中记录了所有ts文件的地址。这种模式允许客户端访问全部内容。上面的例子中就是一个点播模式下的m3u8的结构。 +- **Live** 模式就是实时生成M3u8和ts文件。它的索引文件一直处于动态变化的,播放的时候需要不断下载二级index文件,以获得最新生成的ts文件播放视频。如果一个二级index文件的末尾没有#EXT-X-ENDLIST标志,说明它是一个Live视频流。 + +客户端在播放VOD模式的视频时其实只需要下载一次一级index文件和二级index文件就可以得到所有ts文件的下载地址,除非客户端进行比特率切换,否则无需再下载任何index文件,只需顺序下载ts文件并播放就可以了。但是Live模式下略有不同,因为播放的同时,新ts文件也在被生成中,所以客户端实际上是下载一次二级index文件,然后下载ts文件,再下载二级index文件(这个时候这个二级index文件已经被重写,记录了新生成的ts文件的下载地址),再下载新ts文件,如此反复进行播放。 + +### 单码率适配流(Media Playlist) + +[http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8](https://link.jianshu.com?t=http%3A%2F%2Fwww.modrails.com%2Fvideos%2Fpassenger_nginx.mov) + +``` +#EXTM3U // m3u8文件头 +#EXT-X-VERSION:3 // 协议版本,不写默认是1 +#EXT-X-MEDIA-SEQUENCE:304240 // 第一个ts文件的序列号 +#EXT-X-TARGETDURATION:10 // 每个ts文件的最大时长 +#EXTINF:10.000, // 每个ts文件的持续时间 +cctv1hd-1585920024000.ts // ts文件的url +#EXTINF:10.000, +cctv1hd-1585920034000.ts +#EXTINF:10.000, +cctv1hd-1585920044000.ts +#EXTINF:10.000, +cctv1hd-1585920054000.ts +#EXTINF:10.000, +cctv1hd-1585920064000.ts +#EXTINF:10.000, +cctv1hd-1585920074000.ts +``` + + + + + +## HLS 的优势 + +- 客户端支持简单, 只需要支持 HTTP 请求即可, HTTP 协议无状态, 只需要按顺序下载媒体片段即可. +- 使用 HTTP 协议网络兼容性好, HTTP 数据包也可以方便地通过防火墙或者代理服务器, CDN 支持良好. +- Apple 的全系列产品支持, 由于 HLS 是苹果提出的, 所以在 Apple 的全系列产品包括 iphone, ipad, safari 都不需要安装任何插件就可以原生支持播放 HLS, 现在, Android 也加入了对 HLS 的支持. +- 自带多码率自适应, Apple 在提出 HLS 时, 就已经考虑了码流自适应的问题. + +## HLS 的劣势 + +- 相比 RTMP 这类长连接协议, 延时较高, 难以用到互动直播场景.HLS 理论延时 = 1 个切片的时长 + 0-1个 td (td 是 EXT-X-TARGETDURATION, 可简单理解为播放器取片的间隔时间) + 0-n 个启动切片(苹果官方建议是请求到 3 个片之后才开始播放) + 播放器最开始请求的片的网络延时(网络连接耗时)。为了追求低延时效果, 可以将切片切的更小, 取片间隔做的更小, 播放器未取到 3 个片就启动播放. 但是, 这些优化方式都会增加 HLS 不稳定和出现错误的风险. +- 对于点播服务来说, 由于 TS 切片通常较小, 海量碎片在文件分发, 一致性缓存, 存储等方面都有较大挑战. + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/VideoDevelopment/\346\265\201\345\252\222\344\275\223\351\200\232\344\277\241\345\215\217\350\256\256.md" "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/\346\265\201\345\252\222\344\275\223\351\200\232\344\277\241\345\215\217\350\256\256.md" similarity index 100% rename from "VideoDevelopment/\346\265\201\345\252\222\344\275\223\351\200\232\344\277\241\345\215\217\350\256\256.md" rename to "VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/\346\265\201\345\252\222\344\275\223\351\200\232\344\277\241\345\215\217\350\256\256.md" From 6910f48f6e2d8556bdd9d08c9db07dba6ada021d Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 6 Apr 2020 21:46:08 +0800 Subject: [PATCH 017/213] add notes --- README.md | 10 ++++++++-- .../DASH.md" | 6 ------ ...351\200\232\344\277\241\345\215\217\350\256\256.md" | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 82ccb40e..2aeb5d5f 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,10 @@ Android学习笔记 - [DNS及HTTPDNS][23] - [DLNA简介][24] - [AudioTrack简介][214] - - [流媒体通信协议][224] + - [流媒体协议][224] + - [流媒体通信协议][246] + - [HLS][247] + - [DASH][248] - [ExoPlayer][216] - [1. ExoPlayer简介.md][217] - [2. ExoPlayer MediaSource简介][218] @@ -499,7 +502,7 @@ Android学习笔记 [221]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/ExoPlayer/5.%20ExoPlayer%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E4%B9%8BPlayerView.md "5. ExoPlayer源码分析之PlayerView" [222]: https://github.com/CharonChui/AndroidNote/blob/master/BasicKnowledge/%E5%8F%8D%E7%BC%96%E8%AF%91.md "反编译" [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/blob/master/VideoDevelopment/%E6%B5%81%E5%AA%92%E4%BD%93%E9%80%9A%E4%BF%A1%E5%8D%8F%E8%AE%AE.md "流媒体通信协议" +[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/blob/master/VideoDevelopment/MP4%E6%A0%BC%E5%BC%8F%E8%AF%A6%E8%A7%A3.md "MP4格式详解" [226]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/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 "关键帧" @@ -521,6 +524,9 @@ Android学习笔记 [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" +[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%E9%80%9A%E4%BF%A1%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" diff --git "a/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/DASH.md" "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/DASH.md" index bb951503..a9969659 100644 --- "a/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/DASH.md" +++ "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/DASH.md" @@ -60,12 +60,6 @@ Segments可以包含任何媒体数据,关于容器,官方提供了两种建 SAP:Stream Acess Point,可以简单理解为I帧,每个Segment的第一个帧都是SAP,因此Seek时可直接Seek到某一个Segment的起始位置,利用Init Segment+Seek到的某个Segment的数据,在解封装后可实现完美解码。一般来说,同一个Adaptation set中的多个Representation是Segment Align的(当Adaptation set的属性@segmentAlignment不为false时),因此,当从Representation A切换到Representation B时,如果当前Representation A的第N个Segment已经下载完成,切换时直接下载Representation B的第N+1个Segment即可。 - - - - -![dash_compare2.png](https://raw.githubusercontent.com/CharonChui/Pictures/master/dash_compare2.png) - 码率自适应切换算法 - 基于带宽的码率自适应切换算法 diff --git "a/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/\346\265\201\345\252\222\344\275\223\351\200\232\344\277\241\345\215\217\350\256\256.md" "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/\346\265\201\345\252\222\344\275\223\351\200\232\344\277\241\345\215\217\350\256\256.md" index b4fbe002..6521d7ff 100644 --- "a/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/\346\265\201\345\252\222\344\275\223\351\200\232\344\277\241\345\215\217\350\256\256.md" +++ "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/\346\265\201\345\252\222\344\275\223\351\200\232\344\277\241\345\215\217\350\256\256.md" @@ -1,6 +1,6 @@ 流媒体通信协议 === - + 流媒体(Streaming Media)是指采用流式传输技术在网络上连续实时播放的媒体格式,如音频、视频或多媒体文件,采用流媒体技术使得数据包得以像流水一样发送, 如果没有流媒体技术, 那么我们就要像以前用迅雷下电影一样, 下载整个影片才能观看。 @@ -78,7 +78,7 @@ Smooth Streaming --- 微软提供的一套解决方案,是IIS的媒体服务扩张,用于支持基于HTTP的自适应串流,它的文件切片为mp4,索引文件为ism/ismc。 - +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/dash_compare2.png) ***视频编码标准和音频编码标准是`H.264`和`AAC`,这两种标准分别是当今实际应用中编码效率最高的视频标准和音频标准。*** From bfd962ed5b3fa9ba05f5e372517e734f8fe58658 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 8 Apr 2020 10:47:47 +0800 Subject: [PATCH 018/213] update --- .../DASH.md" | 81 +++++++++++++++++-- .../HLS.md" | 6 +- 2 files changed, 76 insertions(+), 11 deletions(-) diff --git "a/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/DASH.md" "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/DASH.md" index a9969659..fc9c87fe 100644 --- "a/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/DASH.md" +++ "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/DASH.md" @@ -24,6 +24,32 @@ DASH的整个流程: ![image-20200406164238038](https://raw.githubusercontent.com/CharonChui/Pictures/master/mpd_hierarchical_data.png) + + +```xml + + + + + + + + + + + + + + + + + + + +``` + + + ### MPD DASH采用3GPP AHS中定义的MPD(Media Presentation Description)作为媒体文件的描述文件(manifest),作用类似HLS的m3u8文件。MPD文件以XML格式组织,用于描述segment的信息,比如时间、url、视频分辨率、码率等。 @@ -79,14 +105,6 @@ Segments可以包含任何媒体数据,关于容器,官方提供了两种建 -## DASH地址 - -http://ftp.itec.aau.at/datasets/mmsys12/BigBuckBunny/MPDs/BigBuckBunnyNonSeg_2s_isoffmain_DIS_23009_1_v_2_1c2_2011_08_30.mpd - -http://www-itec.uni-klu.ac.at/ftp/datasets/mmsys12/BigBuckBunny/bunny_2s/bunny_2s_50kbit/bunny_50kbit_dashNonSeg.mp4 - - - ## fMP4 fMP4(fragmented MP4),可以简单理解为分片化的MP4,是DASH采用的媒体文件格式,文件扩展名通常为(.m4s或直接用.mp4)。 @@ -147,6 +165,53 @@ DASH和HLS之间的另一个关键区别是它支持DRM。可是,在DASH中不 +### 测试流 + +- HEVC HLS with fMP4: [http://bitmovin-a.akamaihd.net/content/dataset/multi-codec/hevc/stream_fmp4.m3u8](https://bitmovin-a.akamaihd.net/content/dataset/multi-codec/hevc/stream_fmp4.m3u8) + + + +- HEVC HLS with TS (not supported by Apple): [http://bitmovin-a.akamaihd.net/content/dataset/multi-codec/hevc/stream_ts.m3u8](https://bitmovin-a.akamaihd.net/content/dataset/multi-codec/hevc/stream_ts.m3u8) + + [https://bitmovin-a.akamaihd.net/content/playhouse-vr/m3u8s/105560.m3u8](https://bitmovin-a.akamaihd.net/content/playhouse-vr/m3u8s/105560.m3u8) + +- HEVC MPEG-DASH: [http://bitmovin-a.akamaihd.net/content/dataset/multi-codec/hevc/stream.mpd](https://bitmovin-a.akamaihd.net/content/dataset/multi-codec/hevc/stream.mpd) + +- Multi-Codec MPEG-DASH (AVC/H.264, HEVC/H.265, VP9): http://bitmovin-a.akamaihd.net/content/dataset/multi-codec/stream.mpd + + https://bitmovin-a.akamaihd.net/content/playhouse-vr/mpds/105560.mpd + +- VP9 MPEG-DASH: http://bitmovin-a.akamaihd.net/content/dataset/multi-codec/stream_vp9.mpd + + + + + + + + + + + + + + + + + + + + + + + + + +在线测试播放器: + +http://demo.theoplayer.com/test-your-stream-with-statistics + + 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 62acc949..3aede2fa 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" @@ -4,8 +4,8 @@ HLS协议规定: -- 视频的封装格式是TS -- 视频的编码格式为H264,音频编码格式为MP3、AAC或者AC-3 +- 视频的封装格式是ts(16年发布也支持了fMP4) +- 视频的编码格式为H264、H265、VP9(17年支持H265、VP9),音频编码格式为MP3、AAC或者AC-3 - 除了TS视频文件本身,还定义了用来控制播放的m3u8文件(文本文件) HLS协议由三部分组成: @@ -41,7 +41,7 @@ TS(Transport Stream),全程为MPEG2-TS。它的特点就是要求从视频流 ### 多码率适配流(Master Playlist) - + 客户端播放HLS视频流的逻辑其实非常简单,先下载一级Index file,它里面记录了二级索引文件(Alternate-A、Alternate-B、Alternate-C)的地址,然后客户端再去下载二级索引文件,二级索引文件中又记录了TS文件的下载地址,这样客户端就可以按顺序下载TS视频文件并连续播放。 From 98afb02c9ad3e638be5bc347e4505b60de4ede8b Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 22 Apr 2020 21:11:03 +0800 Subject: [PATCH 019/213] update --- README.md | 13 ++++---- .../SurfaceView\344\270\216TextureView.md" | 12 +++++-- .../DASH.md" | 32 +++++++++++++++++-- ...32\344\277\241\345\215\217\350\256\256.md" | 4 +++ 4 files changed, 50 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 2aeb5d5f..ae74c648 100644 --- a/README.md +++ b/README.md @@ -276,8 +276,8 @@ Android学习笔记 - [反编译][222] -[1]: https://github.com/CharonChui/AndroidNote/blob/master/SourceAnalysis/%E8%87%AA%E5%AE%9A%E4%B9%89View%E8%AF%A6%E8%A7%A3.md "自定义View详解" -[2]: https://github.com/CharonChui/AndroidNote/blob/master/SourceAnalysis/Activity%E7%95%8C%E9%9D%A2%E7%BB%98%E5%88%B6%E8%BF%87%E7%A8%8B%E8%AF%A6%E8%A7%A3.md "Activity界面绘制过程详解" +[1]: https://github.com/CharonChui/AndroidNote/blob/master/SourceAnalysis/%E8%87%AA%E5%AE%9A%E4%B9%89View%E8%AF%A6%E8%A7%A3.md "自定义View详解" +[2]: https://github.com/CharonChui/AndroidNote/blob/master/SourceAnalysis/Activity%E7%95%8C%E9%9D%A2%E7%BB%98%E5%88%B6%E8%BF%87%E7%A8%8B%E8%AF%A6%E8%A7%A3.md "Activity界面绘制过程详解" [3]: https://github.com/CharonChui/AndroidNote/blob/master/SourceAnalysis/Activity%E5%90%AF%E5%8A%A8%E8%BF%87%E7%A8%8B.md "Activity启动过程" [4]: https://github.com/CharonChui/AndroidNote/blob/master/SourceAnalysis/Android%20Touch%E4%BA%8B%E4%BB%B6%E5%88%86%E5%8F%91%E8%AF%A6%E8%A7%A3.md "Android Touch事件分发详解" [5]: https://github.com/CharonChui/AndroidNote/blob/master/SourceAnalysis/AsyncTask%E8%AF%A6%E8%A7%A3.md "AsyncTask详解" @@ -518,7 +518,8 @@ Android学习笔记 [237]: 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 "6.OpenGL ES绘制矩形及圆形" [238]: 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 "7.OpenGL ES着色器语言GLSL" [239]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/8.GLES%E7%B1%BB%E5%8F%8AMatrix%E7%B1%BB.md "8.GLES类及Matrix类" -[240]: "https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/9.OpenGL%20ES%E7%BA%B9%E7%90%86.md "9.OpenGL ES纹理" + +[240]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/9.OpenGL%20ES%E7%BA%B9%E7%90%86.md "9.OpenGL ES纹理" [241]: 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播放视频" [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 "弹幕" @@ -541,13 +542,13 @@ License === Copyright (C) 2013 Charon Chui - + Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - + http://www.apache.org/licenses/LICENSE-2.0 - + Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git "a/VideoDevelopment/SurfaceView\344\270\216TextureView.md" "b/VideoDevelopment/SurfaceView\344\270\216TextureView.md" index 4234918a..de765b86 100644 --- "a/VideoDevelopment/SurfaceView\344\270\216TextureView.md" +++ "b/VideoDevelopment/SurfaceView\344\270\216TextureView.md" @@ -82,15 +82,21 @@ SurfaceTexture.OnFrameAvailableListener用于通知TextureView内容流有新图 SurfaceTexture可以用作非直接输出的内容流,这样就提供二次处理的机会。与SurfaceView直接输出相比,这样会有若干帧的延迟。同时,由于它本身管理BufferQueue,因此内存消耗也会稍微大一些。 TextureView是一个可以把内容流作为外部纹理输出在上面的View, 它本身需要是一个硬件加速层。 - ### SurfaceTexture -SurfaceTexture是Surface和OpenGL ES(GLES)纹理的组合。SurfaceTexture用于提供输出到GLES 纹理的Surface。 +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 包含一个应用是其使用方的BufferQueue。当生产方将新的缓冲区排入队列时,onFrameAvailable() 回调会通知应用。然后,应用调用updateTexImage(),这会释放先前占有的缓冲区,从队列中获取新缓冲区并执行EGL调用,从而使GLES可将此缓冲区作为外部纹理使用。 ## SurfaceView vs TextureView + + +简单地说,SurfaceView是一个有自己Surface的View。它的渲染可以放在单独线程而不是主线程中。其缺点是不能做变形和动画。SurfaceTexture可以用作非直接输出的内容流,这样就提供二次处理的机会。与SurfaceView直接输出相比,这样会有若干帧的延迟。同时,由于它本身管理BufferQueue,因此内存消耗也会稍微大一些。TextureView是一个可以把内容流作为外部纹理输出在上面的View。它本身需要是一个硬件加速层。事实上TextureView本身也包含了SurfaceTexture。它与SurfaceView+SurfaceTexture组合相比可以完成类似的功能(即把内容流上的图像转成纹理,然后输出)。区别在于TextureView是在View hierachy中做绘制,因此一般它是在主线程上做的(在Android 5.0引入渲染线程后,它是在渲染线程中做的)。而SurfaceView+SurfaceTexture在单独的Surface上做绘制,可以是用户提供的线程,而不是系统的主线程或是渲染线程。 + + 与 SurfaceView 相比,TextureView 具有更出色的 Alpha 版和旋转处理能力,但在视频上以分层方式合成界面元素时,SurfaceView 具有性能方面的优势。当客户端使用 SurfaceView 呈现内容时,SurfaceView 会为客户端提供单独的合成层。如果设备支持,SurfaceFlinger 会将单独的层合成为硬件叠加层。当客户端使用 TextureView 呈现内容时,界面工具包会使用 GPU 将 TextureView 的内容合成到 View 层次结构中。对内容进行的更新可能会导致其他 View 元素重绘,例如,如果其他 View 位于 TextureView 上方。View 呈现完成后,SurfaceFlinger 会合成应用界面层和所有其他层,以便每个可见像素合成两次。 ***注意:受 DRM 保护的视频只能在叠加平面上呈现。支持受保护内容的视频播放器必须使用 SurfaceView 进行实现。*** @@ -113,7 +119,7 @@ SurfaceTexture 包含一个应用是其使用方的BufferQueue。当生产方将 - +​ --- - 邮箱 :charon.chui@gmail.com diff --git "a/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/DASH.md" "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/DASH.md" index fc9c87fe..0ebf8bd3 100644 --- "a/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/DASH.md" +++ "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/DASH.md" @@ -1,6 +1,6 @@ # 简介 -除了前面将的Apple的HLS,还有Adobe HTTP Dynamic Streaming (HDS)、Microsoft Smooth Streaming (MSS)。他们各家的协议原理大致相同,但是格式又不一样,也无法兼容,所以Moving Picture Expert Group (MPEG) 就把大家叫到了一起,呼吁大家一起来制定一个标准的,然后就有了[MPEG-DASH](https://www.encoding.com/mpeg-dash/),它的主要目标是形成IP网络承载单一格式的流媒体并提供高效与高质量服务的统一方案,解决多制式传输方案(HTTP Live Streaming, Microsoft Smooth Streaming, HTTP Dynamic Streaming)并存格局下的存储与服务能力浪费、运营高成本与复杂度、系统间互操作弱等问题。 +除了前面讲的Apple的HLS,还有Adobe HTTP Dynamic Streaming (HDS)、Microsoft Smooth Streaming (MSS)。他们各家的协议原理大致相同,但是格式又不一样,也无法兼容,所以Moving Picture Expert Group (MPEG) 就把大家叫到了一起,呼吁大家一起来制定一个标准的,然后就有了[MPEG-DASH](https://www.encoding.com/mpeg-dash/),它的主要目标是形成IP网络承载单一格式的流媒体并提供高效与高质量服务的统一方案,解决多制式传输方案(HTTP Live Streaming, Microsoft Smooth Streaming, HTTP Dynamic Streaming)并存格局下的存储与服务能力浪费、运营高成本与复杂度、系统间互操作弱等问题。 [DASH(MPEG-DASH)](https://mpeg.chiariglione.org/standards/mpeg-dash/)全称为Dynamic Adaptive Streaming over HTTP.是由MPEG和ISO批准的独立于供应商的国际标准,它是一种基于HTTP的使用TCP传输协议的流媒体传输技术。MPEG-DASH是一种自适应比特率流技术,可根据实时网络状况实现动态自适应下载。和HLS, HDS技术类似, 都是把视频分割成一小段一小段, 通过HTTP协议进行传输,客户端得到之后进行播放;不同的是MPEG-DASH支持MPEG-2 TS、MP4(最新的HLS也支持了MP4)等多种格式, 可以将视频按照多种编码切割, 下载下来的媒体格式既可以是ts文件也可以是mp4文件,MPEG—DASH技术与编解码器无关,可使用H.265,H.264,VP9等任何编解码器进行编码。 @@ -105,8 +105,30 @@ Segments可以包含任何媒体数据,关于容器,官方提供了两种建 +## 为什么使用DASH + +- DASH支持多种编码,支持H.265、H.264、VP9等。 + +- DASH支持MultiDRM,支持PlayReady、Widewine,采用通用加密技术,支持终端自带DRM,可以大幅度降低DRM投资成本。 + +- DASH支持多种文件封装,支持MPEG-4、MPEG-2 TS。 + +- DASH支持多种CDN对接,采用相同的封装描述对接多厂家CDN。 + +- DASH支持直播、点播、录制等丰富的视频特性。 + +- DASH支持动态码率适配Adaptive Bitrate (ABR) ,支持多码率平滑切换。 + +- DASH支持缩略型描述以支持快速启动。 + + + ## fMP4 +[MP4和fMP4的详细介绍](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/MP4%E6%A0%BC%E5%BC%8F%E8%AF%A6%E8%A7%A3.md) + + + fMP4(fragmented MP4),可以简单理解为分片化的MP4,是DASH采用的媒体文件格式,文件扩展名通常为(.m4s或直接用.mp4)。 ![fMP4](https://img-blog.csdn.net/20171107114807709?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveXVlX2h1YW5n/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) @@ -115,6 +137,10 @@ fMP4(fragmented MP4),可以简单理解为分片化的MP4,是DASH采用 ### fMP4与ts的区别 +最主要的就是.ts文件不提供关于时长等信息,你无法在ts文件中去实现音视频的seek操作。fmp4不同于ts,它是提供了时长等信息,可以执行seek到指定位置。 + + + ### 媒体数据与元数据的分离 在mp4格式中,元数据可以和媒体数据很好地分开存储,后者都在mdat box中,而在ts中,诸多es流和header/metadata信息是复用在一起的。 @@ -185,9 +211,11 @@ DASH和HLS之间的另一个关键区别是它支持DRM。可是,在DASH中不 +参考: +[HLS,MPEG-DASH - What is ABR?](http://telestreamblog.telestream.net/2017/05/what-is-abr/) - +[B站我们为什么使用DASH](https://www.bilibili.com/read/cv855111) diff --git "a/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/\346\265\201\345\252\222\344\275\223\351\200\232\344\277\241\345\215\217\350\256\256.md" "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/\346\265\201\345\252\222\344\275\223\351\200\232\344\277\241\345\215\217\350\256\256.md" index 6521d7ff..644494bf 100644 --- "a/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/\346\265\201\345\252\222\344\275\223\351\200\232\344\277\241\345\215\217\350\256\256.md" +++ "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/\346\265\201\345\252\222\344\275\223\351\200\232\344\277\241\345\215\217\350\256\256.md" @@ -54,6 +54,8 @@ HLS的整体流程为: 视频源文件 -> 服务器(编码器、流分割器) -> 分发器(索引文件、*.ts) -> 传输网络 -> 客户端 +[HLS的详细介绍](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) + HDS --- `Http Dynamic Streaming`:是一个由Adobe公司模仿HLS协议提出的另一个基于Http的流媒体传输协议。其模式与HLS类似,也是索引文件和媒体切片文件结合的下载方式,但是又要比HLS协议更复杂。 @@ -73,6 +75,8 @@ DASH的整个流程: 内容生成服务器(编码模块、封装模块) -> 流媒体服务器(MPD、媒体文件、HTTP服务器) <-> DASH客户端(控制引擎、媒体引擎、HTTP接入容器) +[DASH详细介绍](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) + Smooth Streaming --- From 5f18eddee276ecc18324d27c4fa6c1985df06861 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 27 May 2020 16:32:01 +0800 Subject: [PATCH 020/213] update notes --- "VideoDevelopment/CDN\345\217\212PCDN.md" | 147 ++++++++++++++---- .../DLNA\347\256\200\344\273\213.md" | 9 +- .../P2P\346\212\200\346\234\257/P2P.md" | 0 ...47\220\206_NAT\347\251\277\351\200\217.md" | 71 +++++++++ .../\345\205\263\351\224\256\345\270\247.md" | 38 ++++- ...47\350\203\275\344\274\230\345\214\226.md" | 3 +- ...32\344\277\241\345\215\217\350\256\256.md" | 12 +- .../AV1.md" | 101 ++++++++++++ .../H264.md" | 101 ++++++++++++ .../H265.md" | 101 ++++++++++++ ...72\347\241\200\347\237\245\350\257\206.md" | 4 + 11 files changed, 540 insertions(+), 47 deletions(-) rename VideoDevelopment/P2P.md => "VideoDevelopment/P2P\346\212\200\346\234\257/P2P.md" (100%) create mode 100644 "VideoDevelopment/P2P\346\212\200\346\234\257/P2P\345\216\237\347\220\206_NAT\347\251\277\351\200\217.md" create mode 100644 "VideoDevelopment/\350\247\206\351\242\221\347\274\226\347\240\201/AV1.md" create mode 100644 "VideoDevelopment/\350\247\206\351\242\221\347\274\226\347\240\201/H264.md" create mode 100644 "VideoDevelopment/\350\247\206\351\242\221\347\274\226\347\240\201/H265.md" diff --git "a/VideoDevelopment/CDN\345\217\212PCDN.md" "b/VideoDevelopment/CDN\345\217\212PCDN.md" index 084186b0..672f475e 100644 --- "a/VideoDevelopment/CDN\345\217\212PCDN.md" +++ "b/VideoDevelopment/CDN\345\217\212PCDN.md" @@ -39,14 +39,11 @@ CDN网络中包含的功能实体包括以下部分: - 内容缓存:CDN网络节点,位于用户接入点,是面向最终用户的内容提供设备,可缓存静态Web内容和流媒 体内容,实现内容的边缘传播和存储,以便用户的就近访问。它借助于建立索引、缓存、流分裂、组播(Multicast)等技术,将内容发布或投递到距离用户最近的远程服务点(POP)处。 内容分发包含从内容源到CDN边缘的Cache的过程。从实现上,有两种主流的内容分发技术:PUSH和PULL。 -PUSH是一种主动分发的技术。通常,PUSH由内容管理系统发起,将内容从源或者中心媒体资源库分发到各边缘的 -Cache节点。分发的协议可以采用Http/ftp等。通过PUSH分发的内容一般是比较热点的内容, +PUSH是一种主动分发的技术。通常,PUSH由内容管理系统发起,将内容从源或者中心媒体资源库分发到各边缘的Cache节点。分发的协议可以采用Http/ftp等。通过PUSH分发的内容一般是比较热点的内容, 这些内容通过PUSH方式预分发到边缘Cache,可以实现有针对的内容提供。 对于PUSH分发需要考虑的主要问题是分发策略,即在什么时候分发什么内容。一般来说, 内容分发可以由CP(内容提供商)或者CDN内容管理员人工确定,也可以通过智能的方式决定, -即所谓的智能分发,它根据用户访问的统计信息,以及预定义的内容分发的规则,确定内容分发的过程PULL是一种被 -动的分发技术,PULL分发通常由用户请求驱动。当用户请求的内容在本地的边缘 Cache上不存在(未命中)时, -Cache启动PUL方法从内容源或者其他CDN节点实时获取内容。在PULL方式下,内容的分发是按需的。 +即所谓的智能分发,它根据用户访问的统计信息,以及预定义的内容分发的规则,确定内容分发的过程PULL是一种被动的分发技术,PULL分发通常由用户请求驱动。当用户请求的内容在本地的边缘 Cache上不存在(未命中)时,Cache启动PULL方法从内容源或者其他CDN节点实时获取内容。在PULL方式下,内容的分发是按需的。 - 内容交换机:处于用户接入集中点,可以均衡单点多个内容缓存设备的负载,并对内容进行缓存负载平衡及访问控制。 - 内容路由器:负责将用户的请求调度到适当的设备上。它是整体性的网络负载均衡技术,通过内容路由器中的重定向(DNS)机制,在多个远程POP上均衡用户的请求, 以使用户请求得到最近内容源的响应。CDN负载均衡系统实现CDN的内容路由功能。它的作用是将用户的请求导向 @@ -106,39 +103,49 @@ CDN的负载均衡和分布式存储技术,可以加强网站的可靠性, ### 原理 -CDN的工作原理就是将您源站的资源缓存到位于全国各地的CDN节点上,用户请求资源时,就近返回节点上缓存的 -资源,而不需要每个用户的请求都回您的源站获取,避免网络拥塞、分担源站压力,保证用户访问资源的速度和体验。 +CDN的工作原理就是将您源站的资源缓存到位于全国各地的CDN节点上,用户请求资源时,就近返回节点上缓存的资源,而不需要每个用户的请求都回您的源站获取,避免网络拥塞、分担源站压力,保证用户访问资源的速度和体验。 下面分别是不用CDN以及使用CDN时的访问流程: ![image](https://github.com/CharonChui/Pictures/blob/master/dns_no_cdn.png?raw=true) ![image](https://github.com/CharonChui/Pictures/blob/master/dns_cdn.png?raw=true) -上面图中在使用CDN时,server-isp-DNS服务器不是直接把域名做A记录映射到源站,而是CNAME记录到调度中心, -调度中心根据用户请求的来源,选择一个最近的CDN节点,主要的流程如下: +上面图中在使用CDN时,server-isp-DNS服务器不是直接把域名做A记录映射到源站,而是CNAME记录到调度中心,调度中心根据用户请求的来源,选择一个最近的CDN节点,主要的流程如下: -1. 当用户点击网站页面上的内容URL,经过本地DNS系统解析,DNS系统会最终将域名的解析权交给CNAME指向的CDN -专用DNS服务器。 +1. 当用户点击网站页面上的内容URL,经过本地DNS系统解析,DNS系统会最终将域名的解析权交给CNAME指向的CDN专用DNS服务器。 2. CDN的DNS服务器将CDN的全局负载均衡设备IP地址返回用户。 3. 用户向CDN的全局负载均衡设备发起内容URL访问请求。 -4. CDN全局负载均衡设备根据用户IP地址,以及用户请求的内容URL,选择一台用户所属区域的区域负载均衡设备, -告诉用户向这台设备发起请求。 +4. CDN全局负载均衡设备根据用户IP地址,以及用户请求的内容URL,选择一台用户所属区域的区域负载均衡设备,告诉用户向这台设备发起请求。 5. 区域负载均衡设备会为用户选择一台合适的缓存服务器提供服务,选择的依据包括:根据用户IP地址, -判断哪一台服务器距用户最近;根据用户所请求的URL中携带的内容名称,判断哪一台服务器上有用户所需内容; -查询各个服务器当前的负载情况,判断哪一台服务器尚有服务能力。基于以上这些条件的综合分析之后, +判断哪一台服务器距用户最近;根据用户所请求的URL中携带的内容名称,判断哪一台服务器上有用户所需内容;查询各个服务器当前的负载情况,判断哪一台服务器尚有服务能力。基于以上这些条件的综合分析之后, 区域负载均衡设备会向全局负载均衡设备返回一台缓存服务器的IP地址。 6. 全局负载均衡设备把服务器的IP地址返回给用户。 -7. 用户向缓存服务器发起请求,缓存服务器响应用户请求,将用户所需内容传送到用户终端。如果这台缓存服务器 -上并没有用户想要的内容,而区域均衡设备依然将它分配给了用户,那么这台服务器就要向它的上一级缓存服务器 -请求内容,直至追溯到网站的源服务器将内容拉到本地。 +7. 用户向缓存服务器发起请求,缓存服务器响应用户请求,将用户所需内容传送到用户终端。如果这台缓存服务器上并没有用户想要的内容,而区域均衡设备依然将它分配给了用户,那么这台服务器就要向它的上一级缓存服务器请求内容,直至追溯到网站的源服务器将内容拉到本地。 ### 网站接入CDN -- 将需要加速的域名,添加一个由CDN提供的CNAME。这样用户在访问加速域名时,实际上会访问到这个CNAME。 -- CNAME在解析时会使用CDN的权威域名服务器。在那里根据DNS,去判断用户的物理位置和运营商情况等, -然后返回边缘节点服务器ip客户端。 -- 客户端再次请求返回的ip,边缘节点服务器由本地负载均衡服务器根据用户请求内容、节点服务器状态等动态因素 -去判断哪台服务器可以提供服务,通过302重定向到那台服务器。 +使用CDN的方法很简单,只需要修改自己的DNS解析,设置一个CNAME指向CDN服务商即可。 + +用户访问未使用CDN缓存资源的过程为: + +- 浏览器通过前面提到的过程对域名进行解析,以得到此域名对应的IP地址; +- 浏览器使用所得到的IP地址,向域名的服务主机发出数据访问请求; +- 服务器向浏览器返回响应数据 + +使用CDN后: + +- 当用户点击网站页面上的内容URL,经过本地DNS系统解析,DNS系统会最终将域名的解析权交给CNAME指向的CDN专用DNS服务器。 +- CDN的DNS服务器将CDN的全局负载均衡设备IP地址返回用户。 +- 用户向CDN的全局负载均衡设备发起内容URL访问请求。 +- CDN全局负载均衡设备根据用户IP地址,以及用户请求的内容URL,选择一台用户所属区域的区域负载均衡设备,告诉用户向这台设备发起请求。 + +- 区域负载均衡设备会为用户选择一台合适的缓存服务器提供服务,选择的依据包括:根据用户IP地址,判断哪一台服务器距用户最近;根据用户所请求的URL中携带的内容名称,判断哪一台服务器上有用户所需内容;查询各个服务器当前的负载情况,判断哪一台服务器尚有服务能力。基于以上这些条件的综合分析之后,区域负载均衡设备会向全局负载均衡设备返回一台缓存服务器的IP地址。 + +- 全局负载均衡设备把服务器的IP地址返回给用户 + +- 用户向缓存服务器发起请求,缓存服务器响应用户请求,将用户所需内容传送到用户终端。如果这台缓存服务器上并没有用户想要的内容,而区域均衡设备依然将它分配给了用户,那么这台服务器就要向它的上一级缓存服务器请求内容,直至追溯到网站的源服务器将内容拉到本地。 + +上面的字太多,有点绕?通俗点就是用户访问的资源原本是存放在你自己的服务器,通过修改DNS让用户根据IP等情况来选择合适的CDN缓存服务器来获取资源。 ## PCDN @@ -148,22 +155,60 @@ PCDN:(P2P技术和CDN的融合创新),下面是[阿里云PCDN](https://help.al 海量碎片化闲置资源而构建的低成本高品质内容分发网络服务。客户通过集成PCDN SDK接入该服务后能获得等 同(或略高于)CDN的分发质量,同时显著降低分发成本。适用于视频点播、直播、大文件下载等业务场景。 + + +我们知道,在移动终端上,通常上传流量会带来很多负面影响,比如消耗用户流量、更加耗电、频繁读写T卡/ROM影响硬件设备寿命等。阿里云PCDN利用海量的P2P节点资源,使终端能做到只下载而不用上传,大大扩展了P2P CDN的使用范围,这个优势已明显领先于那些完全依靠客户端上传流量而获得P2P效率的P2P产品。 + +P2P节点布局中有路由器、运营商接入层/汇聚层节点等二级节点可以为客户APP提供P2P带宽,不强制要求客户APP上传流量,所以移动端不需要上传流量也可以使用PCDN。 + +另外,PCDN针对各类业务场景做了进一步优化,使之对各场景的支持更加完善,给客户提供了更多差异化价值。在下载业务方面,支持下载速率控制(限速)。在带宽峰期对下载速度进行限制,可以有效控制带宽峰值。对于一些不追求下载速度的后台下载场景,同样可以对下载速度进行控制,使后台下载不至于影响前台的游戏、视频等网络体验。在安全方面,支持https,防止内容被篡改。并且集成了httpdns,保证了调度精准性,同时有效避免了域名劫持的发生。在防盗链的处理上,PCDN除了继承CDN的referer、鉴权策略、IP黑名单等防盗链机制,还从云+端的角度,使SDK跟本地业务结合,实现鉴权逻辑、DRM等,弥补了单纯靠云端鉴权的不足,使防盗链机制更加完善。 + ### CDN存在的问题 传统的CDN技术虽然可以在一定程度上加速流媒体,实现下载、直播和点播。但是其核心仍然是基于 集中服务器的结构,跟地域化管制紧密相连,很难降低其扩展的成本。另外,传统CDN技术在高峰时期 -对突发流量的适应性,容错性等方面仍然存在一定缺陷。随着用户规模的迅速增加,对CDN应用发展提出了较大挑战。 -因部署或租用机房带来的多方面高昂成本和管理压力,学界和业界也研究了将P2P技术融入CDN部署和管理的技术, -以降低运营成本和通信时延。迅雷、优酷、百度、阿里巴巴等公司就在2010年代多次试水用户端运行的P2P众包类 -CDN服务、专用设备,模式为用户自愿以PC或专用设备利用闲置上行带宽充当CDN缓存节点, +对突发流量的适应性,容错性等方面仍然存在一定缺陷。随着用户规模的迅速增加,对CDN应用发展提出了较大挑战。因部署或租用机房带来的多方面高昂成本和管理压力,学界和业界也研究了将P2P技术融入CDN部署和管理的技术,以降低运营成本和通信时延。迅雷、优酷、百度、阿里巴巴等公司就在2010年代多次试水用户端运行的P2P众包类CDN服务、专用设备,模式为用户自愿以PC或专用设备利用闲置上行带宽充当CDN缓存节点, 提供服务并赚取积分,而积分可兑换现金红包、特定商品或服务。 - + P2P技术则是打破了传统的Client/Server模式,是一种基于对等节点非中心化服务的平台方案。 P2P技术发展迅猛,迅速改变了整个互联网传统秩序。“去中性化”符合WEB2.0技术潮流。特别在流媒体领域, 由于采用Peer之间对等计算的模式,大大提高了资源共享的利用率,能在较低的成本下,充分利用空闲时间分 发数据,避免拥塞,提供具备高实时性,和容错性能的流服务。为流媒体服务开辟了一条崭新的道路。 -### P2P简介 + + + + +### PCDN节点及架构 + + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/pcdn_jiedian.png) + + + +**PCDN的基础架构** + +关键组件 + +- index服务:全局调度,把用户请求调度到最佳的机房 + +- ZooKeeper (Global&Local):服务活动情况汇报给调度服务,动态配置更新 + +- Nignx Proxy:支持私有协议的Nginx代理服务,针对不同文件一致性Hash到不同的Channel服务 + +- Channel服务:记录文件和拥有文件的端点地址信息,为下载提供就近的端点地址 + +- Realy服务:服务P2P建立连接和通讯 + +- Hot服务:hot文件发现和推送 + +PCDN架构图: + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/pcdn_jiagou.png) + +### P2P简介 + P2P(Peer to Peer):点对点通信或称为对等联网。每个用户既下载数据,又作为服务器存储数据 并供其他用户下载。他的本质,是一种硬盘的共享,是把每个人电脑上的一部分硬盘,拿出来与其他人共享。 @@ -172,15 +217,49 @@ P2P(Peer to Peer):点对点通信或称为对等联网。每个用户既下载 产品采用的拓扑结构、算法模型不尽相同,缺乏标准体系,应用模式也不清晰。这些问题都阻碍了P2P技术 进一步发展成为运营商级别的可靠技术平台。 +### P2P技术的发展演变过程 + +早期的P2P技术,称为1.0版本,这个版本主要是依靠P2P尽力而为的提供服务,没有服务质量保障。像2000年前后出现的迅雷、BT、电驴等2C的应用。这类应用是互联网上内容传播分享的工具,并没有严谨的服务质量保证,而且从技术层面来说,这样的应用都是基于终端流量的上传,需要同时读写电脑本地的磁盘。 + +经过几年的发展,到2005年左右,发展到P2P的2.0版本,这个版本的主要特征是P2P+CDN,有了一定的服务质量保障。像风行、PPlive、PPS、优酷等视频网站,把P2P和CDN相结合,做了自建的技术方案。但是从技术实现上来讲,还是基于终端的流量互传,主要是PC客户端和Flash这两种形态。 + +到2014年以后,P2P技术发展到了3.0版本,这个版本的特征就是CDN+P2P技术+P2P节点资源,能提供全面的服务质量保障,性能不低于CDN。这个时期有优酷路由宝和迅雷赚钱宝等智能硬件产品面世,吸纳用户家庭和商业场所中闲散的带宽资源,打包成P2P的分发服务。P2P节点自此发展成了共享经济的模型。技术上,使用了智能硬件产品,客户端APP不再需要终端上传和读写磁盘。在业务场景上也支持的比较全面,长视频/短视频点播、大型赛事直播、秀场直播、大文件下载等都能完美支持。质量好,价格低,并且业务场景支持全面,这些因素促成了P2P CDN产品的不断发展,这个阶段积累了较大规模的商业化案例。 + + + +### PCDN保证高质量的基本原理 + +CDN对外服务是单节点、单链路的方式,这要求CDN节点有很高的稳定性。一旦CDN节点出现故障或者链路出现抖动、拥塞等,将势必影响服务质量。而PCDN对外服务是多点对单点、多链路的方式,这种特性使PCDN能够有效避免节点故障、链路网络问题带来的影响,使整个传输更加稳定。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/pcdn_vs_cdn.png) + +PCDN对外服务时,CDN将被当做超级种子,同时也有很多P2P节点提供服务。虽然单个P2P节点的能力弱于CDN,但依赖数量众多的P2P节点和优良的调度算法,可以很好的保证整网的可靠性。CDN在节点水位问题、稳定性、网络波动等问题将要发生之前,P2P调度会尽可能从P2P节点把用户所要的内容返回给SDK,来抑制缓冲和卡顿的情况。所以PCDN相对于CDN,服务质量有所优化。 + +有数据显示,以视频点播业务为例,PCDN的首播时间等同于CDN,流畅性同比CDN平均提升1~3%。在下载业务方面,PCDN的下载速度、下载完成率等指标全面领先CDN。 + +### 四、PCDN保证低价格的基本原理 + +PCDN的带宽分为一、二、三级节点带宽,一级节点带宽为CDN带宽;二级节点带宽为分布全国各地P2P节点提供的带宽,包含接入层节点、路由器等提供的运营商/家庭出口带宽;三级带宽为客户端之间互相分享的带宽。如下图所示,各级节点能力不同,从一级CDN节点到三级客户端节点,根据其自身能力,按网络质量、存储容量、稳定性、计算能力、节点数量、可控性六个基本维度进行划分。其中,CDN各方面能力是最强的,但是节点数量是最少的,成本是最高的。逐一往右推移,网络质量和存储能力、计算能力在不同程度的下降,但是节点数量在提升,成本在降低。 + + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/pcdn_peer.png) + +所以,PCDN在对外提供服务时,调度系统根据质量优先、兼顾成本的原则,对各级节点进行合理配比。以视频点播的业务为例,当播放器开始播放的时候,无论从用户还是从业务角度,都希望尽快拿到数据,不愿意缓冲和等待,这个时候PCDN默认CDN的响应是比较及时,调度系统首先从CDN上拿数据,即首帧时间与CDN等效。在正常播放过程中,播放器预缓冲时,播放器对数据的请求不是很着急,这时优先使用低成本的P2P,如果P2P没有命中或者调度系统评估要很慢才能回传,再回源CDN。通过这样的方法,能有效减少高成本的CDN用量。 + +通过这种方式,根据业务场景和使用量,PCDN的价格一般比CDN降低30%~50%。目前支持按日峰值带宽、月95峰值带宽和日流量三种计费方式。从客户的时间使用情况看,对于一个峰值带宽30G左右的业务,使用PCDN一年相比CDN可以节省至少100万。对于带宽峰值超过100G的业务,成本节省效果将更加可观。 + +PCDN目前已支持视频点播、视频直播、大文件下载各类典型的业务场景,包括但不限于长视频点播、短视频点播、互动娱乐直播、晚会赛事直播、应用市场分发等各类产品,全面支持Android、iOS、OTT、Flash、PC-Client等各种主流平台和主流协议。通过在娱乐、教育、体育、广电等多个行业积累的众多成熟服务客户案例,PCDN对各业务场景的理解与服务也不断升级优化,目前已能支持更多的下载场景、提供更好的防劫持防盗链方案和更好的视频播放体验,让质量和价格不再是矛盾体,达到高质量低成本这一超高性价比的效果,鱼和熊掌兼得。PCDN,期待为您提供更好的服务。 + ### CDN和P2P技术优劣势分析 | | CDN | P2P | -| ------ | ---- | ---- | +| ------ | ---- | ---- | | 可扩展性 | 扩展成本较高 | 低成本扩展 | -| 内容版权 | 可监管 | 不可监管 | +| 内容版权 | 可监管 | 不可监管 | | 用户管理有效性 | 可实现用户的有效管理 | 无法进行有效的用户管理 | | QoS服务 | 可保障服务 | 无法保障 | -| 流量有序性 | 流量区域控制 | 无序无序 | +| 流量有序性 | 流量区域控制 | 无序无序 | 也就是说,P2P和CDN技术在几个关键点上,完全实现互补,如果能将两种技术有效的结合起来, 必然是一种更加完美的组合。于是,一种全新的思路是在CDN网络中,引入P2P技术。通过这种模式, @@ -202,8 +281,10 @@ P2P用户将根据这些规则来完成P2P共享。P2P在边缘层的引入大 一方面,P2P+CDN结合的方式,使得有限的服务能力可以为更多的用户提供流媒体服务。超级种子的存在保证了 服务质量。另一方面,P2P技术的应用也能够更有效地防止因网络的抖动而产生对服务质量的影响。 +​ + +[Content Delivery Acceleration and Cost Reduction with P2P CDN (PCDN)](https://dzone.com/articles/content-delivery-acceleration-and-cost-reduction-w) - --- - 邮箱 :charon.chui@gmail.com diff --git "a/VideoDevelopment/DLNA\347\256\200\344\273\213.md" "b/VideoDevelopment/DLNA\347\256\200\344\273\213.md" index 7abc3486..cbf52924 100644 --- "a/VideoDevelopment/DLNA\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/DLNA\347\256\200\344\273\213.md" @@ -17,7 +17,7 @@ DLNA `DLNA`的骨干成员包括以Intel为首的芯片制造商;以HP为首的PC制造商,以Sony,Panasonic,Sharp,Samsung,LG 为首的家电、消费电子制造商;以CISCO,HUWEI,MOTOROLA,ERICSSON为首的电信设备/移动终端/标准商;一家独大的Microsoft软件/操作系统商等等。 值得注意的有几点: - + 1. DLNA这个东西基本Intel,Microsoft两个领域巨头在推,一个搞芯片,一个搞系统。AMD没出现在2011的promoter名单中;Google来年会不会掺一脚不好说。还有QUALCOMM也参加进来了,这几年的智能手机芯片处理器他家的也比较多,而且他家还有很多专利可以吃。 2. 2011就剩HP一个大PC商了,其他大PC商如Acer,Asus都还不是promoter,他们肯定要抢着加入的。lenovo不仅从promotor名单中消失了,自然也不会是contributor了,和AMD一样。最开始时lenovo是很积极的,在DHWG的时候也是骨干成员,回来中国搞了一个“IGRS闪联”,退出的原因不知道和这个有没有关系。IGRS在很大程度上和DLNA是比较类似的,框架协议和UPnP也是比较像的。 @@ -122,11 +122,12 @@ DLNA架构分为如下图7个层次: - 从DMS/M-DMS至DMP/M-DMP,即使不立即播放。 - 从一个DMS到另一个DMS,这时接收方DMS播放接收媒体内容,表现为一个DMP;也可以不立即播放,可能只是存储或者处理。 + 传输 模式有三种: - 流传输。当DMR/DMP需要实时渲染接收媒体,媒体具时序性。 - 交互传输。不包含时序的媒体,如图片传输。 - - 后台传输。非实时的媒体传输,比如上传下载等。 - +- 后台传输。非实时的媒体传输,比如上传下载等。 + 6. Media Formats媒体格式。格式Formats在这里等同于编码格式Codec,平时我们说的编码格式比如Mpeg-2,AVC,x264就是视频编码格式;PCM,mp3(MPEG-2 Layer 3),aac,flac就是音频编码格式。而avi,rmvb,mkv这些是媒体封装格式,包含视频音频可能还有字幕流。比如一个常见的后缀为mkv的文件,它的视频Codec是x264,音频是aac,它的视音频编码属于Mpeg-4 Codec Family。 7. Remote UI 远程用户接口。 @@ -170,7 +171,7 @@ http://upnp.org/ 填"M-SEARCH * HTTP/1.1/r/n"就是要搜索了;respone别人的搜索就填"HTTP/1.1 200 OK/r/n"。 SSDP第二个要填充的字段是目的地址HOST。比如填上"HOST: 239.255.255.250:1900",就是组播(multicast)搜索,这里239.255.255.250是组播地址,就是说这条消息会给网络里面该组地址的设备发,1900是SSDP协议的端口号。如果HOST地址是特定地址,那这就是单播(unicast)。Respone不填这个字段,他会在ST字段里面填respone address,就是发来搜索信息的设备的地址,Respone消息的话还会发送一个包含自己地址URL的字段,Respone的意思就是跟Searcher说:我好像是你要找的人,我的电话是XXX,详细情况请CALL我。Respone也是UDP单播。 - + 3. 描述(Description) 前面我们说了CP想要一个device更详细的信息,就打给它的URL跟它要。返回来的东西一般是个XML(Extensible Markup Language,是种结构化的数据。和HTML比较像,有tag和data,具体不说了自己去查),描述分为两部分:一个是device description,是device的物理描述,就是说这个device是什么;还有一个是service descriptions,就是device的服务描述了,就是device能干些什么。这些device和device service的描述的格式也是有要求的,开发商也可以自定义,只要符合UPnP Forum的规范。 diff --git a/VideoDevelopment/P2P.md "b/VideoDevelopment/P2P\346\212\200\346\234\257/P2P.md" similarity index 100% rename from VideoDevelopment/P2P.md rename to "VideoDevelopment/P2P\346\212\200\346\234\257/P2P.md" 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" new file mode 100644 index 00000000..43bcfc89 --- /dev/null +++ "b/VideoDevelopment/P2P\346\212\200\346\234\257/P2P\345\216\237\347\220\206_NAT\347\251\277\351\200\217.md" @@ -0,0 +1,71 @@ +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提出申请,而在内部的路由管理和报文传递方式与其他网络没有差异。 + + 对于有Internet访问需求而内部又使用私有地址的网络,就要在组织的出口位置部署NAT网关,在报文离开私网进入Internet时,将源IP替换为公网地址,通常是出口设备的接口地址。一个对外的访问请求在到达目标以后,表现为由本组织出口设备发起,因此被请求的服务端可将响应由Internet发回出口网关。出口网关再将目的地址替换为私网的源主机地址,发回内部。这样一次由私网主机向公网服务端的请求和响应就在通信两端均无感知的情况下完成了。依据这种模型,数量庞大的内网主机就不再需要公有IP地址了。 + + 那么,NAT与此同时也带来一些弊端:首先是,NAT设备会对数据包进行编辑修改,这样就降低了发送数据的效率;此外,各种协议的应用各有不同,有的协议是无法通过NAT的(不能通过NAT的协议还是蛮多的),这就需要通过穿透技术来解决。我们后面会重点讨论穿透技术。 + + + +**虽然实际过程远比这个复杂,但上面的描述概括了NAT处理报文的几个关键特点:** + + + +- 1)网络被分为私网和公网两个部分,NAT网关设置在私网到公网的路由出口位置,双向流量必须都要经过NAT网关; +- 2)网络访问只能先由私网侧发起,公网无法主动访问私网主机; +- 3)NAT网关在两个访问方向上完成两次地址的转换或翻译,出方向做源信息替换,入方向做目的信息替换; +- 4)NAT网关的存在对通信双方是保持透明的; +- 5)NAT网关为了实现双向翻译的功能,需要维护一张关联表,把会话的信息保存下来。 + + + + 简单的背景了解过后,下面介绍下NAT实现的主要方式,以及NAT都有哪些类型。 + + + +### NAT的实现方式 + + + +- 静态NAT: 就是静态地址转换。是指一个公网IP对应一个私有IP,是一对一的转换,同时注意,这里只进行了IP转换,而没有进行端口的转换。 + +- NAPT: 端口多路复用技术。与静态NAT的差别是,NAPT不但要转换IP地址,还要进行传输层的端口转换。具体的表现形式就是,对外只有一个公网IP,通过端口来区别不同私有IP主机的数据。 + + + + + + + + + + + + + + + + + + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file 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 ac0d3d9b..da3274dc 100644 --- "a/VideoDevelopment/\345\205\263\351\224\256\345\270\247.md" +++ "b/VideoDevelopment/\345\205\263\351\224\256\345\270\247.md" @@ -1,23 +1,30 @@ 关键帧 === -MP4是一种格式的规范,这个规范是被ISO机构认证的,你如果要生成一个mp4文件,就必须按照这个规矩来。 +**编码器将多张图像进行编码后生产成一段一段的 GOP ( Group of Pictures ) , 解码器在播放时则是读取一段一段的 GOP 进行解码后读取画面再渲染显示。**GOP ( Group of Pictures) 是一组连续的画面,由一张 I 帧和数张 B / P 帧组成,是视频图像编码器和解码器存取的基本单位,它的排列顺序将会一直重复到影像结束。I 帧是内部编码帧(也称为关键帧),P帧是前向预测帧(前向参考帧),B 帧是双向内插帧(双向参考帧)。简单地讲,I 帧是一个完整的画面,而 P 帧和 B 帧记录的是相对于 I 帧的变化。如果没有 I 帧,P 帧和 B 帧就无法解码。 - -视频压缩中,每一帧代表一副静止的图像,在实际的压缩时,会采取各种算法来减少数据的容量。视频压缩采用的方法是分组,把几帧图像分为一组(GOP(Group of picture)).其中IPB是最常见的。 +在H.264压缩标准中I帧、P帧、B帧用于表示传输的视频画面。 - I帧(Intra coded picture):帧内编码图像帧简称关键帧,这一帧画面的完整保留,解码时只需要本帧数据就可以完成.I帧通常是每个GOP(MPEG所使用的一种视频压缩技术)的第一个,GOP就是指两个I帧之间的距离。 -这里有一个IDR帧的概念需要讲一下,所有的IDR帧都是I帧,但是并不是所有I帧都是IDR帧,IDR帧是I帧的子集。I帧严格定义是帧内编码帧,由于是一个全帧压缩编码帧,通常用I帧表示「关键帧」。IDR是基于I帧的一个扩展,带了控制逻辑,IDR图像都是I帧图像,当解码器解码到IDR图像时,会立即将参考帧队列清空,将已解码的数据全部输出或抛弃。重新查找参数集,开始一个新的序列。这样如果前一个序列出现重大错误,在这里可以获得重新同步的机会。IDR图像之后的图像永远不会使用IDR之前的图像的数据来解码。在H.264编码中,GOP是封闭式的,一个GOP的第一帧都是IDR帧。 - -我们在做直播的时候,想要能达到秒开的效果,因为I帧是能不依赖其他帧独立完成解码的,这就意味着当播放器接收到I帧就能立马渲染出来,而接收到P、B帧则需要等待依赖的帧而不能立马完成解码和渲染,这个期间就会黑屏,所以可以在服务端通过缓存GOP,保证播放端在接入直播时能先获取到I帧马上渲染出画面,从而提高起播速度。在直播服务器中,支持设置一个cache,用于存放GOP,直播服务器缓存了当前的GOP序列,当播放端请求数据的时候,CDN会从I帧返回给客户端,从而保证客户端可以快速进行播放。当然由于缓存的是之前的视频信息,当音频数据到达播放端后,为了音视频同步播放器会进行视频的快进处理。 + 我们在做直播的时候,想要能达到秒开的效果,因为I帧是能不依赖其他帧独立完成解码的,这就意味着当播放器接收到I帧就能立马渲染出来,而接收到P、B帧则需要等待依赖的帧而不能立马完成解码和渲染,这个期间就会黑屏,所以可以在服务端通过缓存GOP,保证播放端在接入直播时能先获取到I帧马上渲染出画面,从而提高起播速度。在直播服务器中,支持设置一个cache,用于存放GOP,直播服务器缓存了当前的GOP序列,当播放端请求数据的时候,CDN会从I帧返回给客户端,从而保证客户端可以快速进行播放。当然由于缓存的是之前的视频信息,当音频数据到达播放端后,为了音视频同步播放器会进行视频的快进处理。 - P帧(Predictive coded picture):预测编码图像帧简称差别帧,这一帧跟之前的一个关键帧或P帧的差别,解码时需要用之前缓存的画面叠加上本帧定义的差别,生成最终的画面。也就是说P帧没有完整的画面数据,只有与前一帧的画面差别的数据。 - B帧(Bidirectionally predicted picture):双向预测编码图像帧简称双向差别帧,也就是B帧记录的是本帧与前后帧的差别。也就是要解码B帧,不仅要取得之前的缓存画面,还要解码之后的画面,通过前后画面与本帧数据的叠加来获取最终的画面。B帧压缩率高,但是解码时会更耗CPU。 - GOP(Group of Pictures)是一组连续的画面,由一张I帧和数张B/P帧组成,是视频图像编码器和解码器存取的基本单位,它的排列顺序将会一直重复到影像结束。 -编码器将多张图像进行编码后生产成一段一段的 GOP ,解码器在播放时则是读取一段一段的GOP进行解码后读取画面再渲染显示。 + 编码器将多张图像进行编码后生产成一段一段的 GOP ,解码器在播放时则是读取一段一段的GOP进行解码后读取画面再渲染显示。 +- IDR(Instantaneous Decoding Refresh)--即时解码刷新。 + I帧:帧内编码帧是一种自带全部信息的独立帧,无需参考其它图像便可独立进行解码,视频序列中的第一个帧始终都是I帧。 + 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 参数进行更新。 + 对IDR帧的处理(与I帧的处理相同):(1) 进行帧内预测,决定所采用的帧内预测模式。(2) 像素值减去预测值,得到残差。(3) 对残差进行变换和量化。(4) 变长编码和算术编码。(5) 重构图像并滤波,得到的图像作为其它帧的参考帧。 + 多参考帧情况下, 举个例子 :有如下帧序列: IPPPP I P PPP ……。按照 3 个参考帧编码。 + 因为“按照 3 个参考帧编码”,所以参考帧队列长度为 3 。 + 遇到第二个 I 时,并不清空参考帧队列,把这个 I 帧加入参考帧队列(当然 I 编码时不用参考帧。)。再检测到其后面的 P 帧时,用到之前的 PPI 三帧做参考了。 +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/video_frame_ipb.jpg) ### 数据压缩比 @@ -31,6 +38,23 @@ P帧和B帧极大的节省了数据量,节省出来的空间可以用来多保 +【为什么会有PTS和DTS的概念】 + +通过上面的描述可以看出:P帧需要参考前面的I帧或P帧才可以生成一张完整的图片,而B帧则需要参考前面I帧或P帧及其后面的一个P帧才可以生成一张完整的图片。这样就带来了一个问题:在视频流中,先到来的 B 帧无法立即解码,需要等待它依赖的后面的 I、P 帧先解码完成,这样一来播放时间与解码时间不一致了,顺序打乱了,那这些帧该如何播放呢?这时就引入了另外两个概念:DTS 和 PTS。 + +【PTS和DTS】 + +先来了解一下PTS和DTS的基本概念: + +DTS(Decoding Time Stamp):即**解码时间戳**,这个时间戳的意义在于告诉播放器该在什么时候解码这一帧的数据。 +PTS(Presentation Time Stamp):即**显示时间戳**,这个时间戳用来告诉播放器该在什么时候显示这一帧的数据。 + +虽然 DTS、PTS 是用于指导播放端的行为,但它们是在编码的时候由编码器生成的。 + +在视频采集的时候是录制一帧就编码一帧发送一帧的,在编码的时候会生成 PTS,这里需要特别注意的是 frame(帧)的编码方式,在通常的场景中,编解码器编码一个 I 帧,然后向后跳过几个帧,用编码 I 帧作为基准帧对一个未来 P 帧进行编码,然后跳回到 I 帧之后的下一个帧。编码的 I 帧和 P 帧之间的帧被编码为 B 帧。之后,编码器会再次跳过几个帧,使用第一个 P 帧作为基准帧编码另外一个 P 帧,然后再次跳回,用 B 帧填充显示序列中的空隙。这个过程不断继续,每 12 到 15 个 P 帧和 B 帧内插入一个新的 I 帧。P 帧由前一个 I 帧或 P 帧图像来预测,而 B 帧由前后的两个 P 帧或一个 I 帧和一个 P 帧来预测,因而编解码和帧的显示顺序有所不同 + + + diff --git "a/VideoDevelopment/\346\222\255\346\224\276\345\231\250\346\200\247\350\203\275\344\274\230\345\214\226.md" "b/VideoDevelopment/\346\222\255\346\224\276\345\231\250\346\200\247\350\203\275\344\274\230\345\214\226.md" index ab28c0f6..ba32b52d 100644 --- "a/VideoDevelopment/\346\222\255\346\224\276\345\231\250\346\200\247\350\203\275\344\274\230\345\214\226.md" +++ "b/VideoDevelopment/\346\222\255\346\224\276\345\231\250\346\200\247\350\203\275\344\274\230\345\214\226.md" @@ -64,7 +64,7 @@ DNS解析加快,通常,DNS解析,意味着要把一个域名为xxx.com解 #### 预加载 -预加载能减少首次缓冲号码,但是预加载不能和当前播放的视频抢下载带宽,需要达到一个合适的阈值再开始下载。 +预加载能减少首次缓冲时间,但是预加载不能和当前播放的视频抢下载带宽,需要达到一个合适的阈值再开始下载。 优先保证封面图等信息完成后再根据云控的当前缓冲的时长等信息来控制是否要启动预加载 @@ -72,7 +72,6 @@ DNS解析加快,通常,DNS解析,意味着要把一个域名为xxx.com解 有关CDN相关可参考[CDN及PCDN](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/CDN%E5%8F%8APCDN.md) - --- - 邮箱 :charon.chui@gmail.com diff --git "a/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/\346\265\201\345\252\222\344\275\223\351\200\232\344\277\241\345\215\217\350\256\256.md" "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/\346\265\201\345\252\222\344\275\223\351\200\232\344\277\241\345\215\217\350\256\256.md" index 644494bf..5eb94854 100644 --- "a/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/\346\265\201\345\252\222\344\275\223\351\200\232\344\277\241\345\215\217\350\256\256.md" +++ "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/\346\265\201\345\252\222\344\275\223\351\200\232\344\277\241\345\215\217\350\256\256.md" @@ -1,11 +1,21 @@ 流媒体通信协议 === - 流媒体(Streaming Media)是指采用流式传输技术在网络上连续实时播放的媒体格式,如音频、视频或多媒体文件,采用流媒体技术使得数据包得以像流水一样发送, 如果没有流媒体技术, 那么我们就要像以前用迅雷下电影一样, 下载整个影片才能观看。 +HTTP +--- + + + +HTTP 视频协议是在互联网普及之后在互联网上看视频的需求下形成的。最初的 HTTP 视频协议,没有任何特别之处,就是通用的 HTTP 文件渐进式下载,但是在这种情况下,视频无法快进或者跳转到文件尚未被下载到的部分,这就对 HTTP 协议提出了范围请求(Range Request)的要求,目前几乎所有 HTTP 服务器都支持范围请求。所谓范围请求指的是请求文件的部分数据,这可以在 HTTP 请求头中通过 Range 字段设置偏移量来实现。 + +这种方式应用于视频点播还可以,用于直播的话实时性较差,延迟也很高,于是,苹果公司又在 HTTP 协议的基础上推出了 HTTP Live Streaming(简称 HLS)这个流媒体传输协议。 + + + RTP --- diff --git "a/VideoDevelopment/\350\247\206\351\242\221\347\274\226\347\240\201/AV1.md" "b/VideoDevelopment/\350\247\206\351\242\221\347\274\226\347\240\201/AV1.md" new file mode 100644 index 00000000..45aa49ec --- /dev/null +++ "b/VideoDevelopment/\350\247\206\351\242\221\347\274\226\347\240\201/AV1.md" @@ -0,0 +1,101 @@ +H264 +=== + + + + + +H.264是一种高性能的视频编解码技术。目前国际上制定视频编解码技术的组织有两个,一个是“国际电联”,它制定的标准有H.261、H.263、H.263+等,另一个是“国际标准化组织(ISO)”它制定的标准有MPEG-1、MPEG-2、MPEG-4等。而H.264则是由两个组织联合组建的联合视频组(JVT)共同制定的新数字视频编码标准,所以它既是ITU-T的H.264,又是ISO/IEC的MPEG-4高级视频编码,而且它将成为MPEG-4标准的第10部分。因此,不论是MPEG-4 AVC、MPEG-4 Part 10,还是ISO/IEC 14496-10,都是指H.264。 + + + + + +## H.264算法的优势 + + + H.264是在MPEG-4技术的基础之上建立起来的,其编解码流程主要包括5个部分:帧间和帧内预测、变换和反变换、量化和反量化、环路滤波、熵编码。 + + H.264/MPEG-4 AVC(H.264)是1995年自MPEG-2视频压缩标准发布以后的最新、最有前途的视频压缩标准。H.264是由ITU-T和ISO/IEC的联合开发组共同开发的最新国际视频编码标准。通过该标准,在同等图象质量下的压缩效率比以前的标准提高了2倍以上,因此,H.264被普遍认为是最有影响力的行业标准。 + +## H.264的优势 + + + H.264在1997年ITU的视频编码专家组提出时被称为H.26L,在ITU与ISO合作研究后被称为MPEG4 Part10或H.264(JVT)。H.264标准的主要目标是:与其它现有的视频编码标准相比,在相同的带宽下提供更加优秀的图象质量。 + +**而,H.264与以前的国际标准如H.263和MPEG-4相比,最大的优势体现在以下四个方面:** + + + +- 将每个视频帧分离成由像素组成的块,因此视频帧的编码处理的过程可以达到块的级别。 +- 采用空间冗余的方法,对视频帧的一些原始块进行空间预测、转换、优化和熵编码(可变长编码)。 +- 对连续帧的不同块采用临时存放的方法,这样,只需对连续帧中有改变的部分进行编码。该算法采用运动预测和运动补偿来完成。对某些特定的块,在一个或多个已经进行了编码的帧执行搜索来决定块的运动向量,并由此在后面的编码和解码中预测主块。 +- 采用剩余空间冗余技术,对视频帧里的残留块进行编码。例如:对于源块和相应预测块的不同,再次采用转换、优化和熵编码。 + + +**具体优势表现为:** + + + +- 低码流:和MPEG2和MPEG4 ASP等压缩技术相比,在同等图像质量下,采用H.264技术压缩后的数据量只有MPEG2的1/8,MPEG4的1/3。显然,H.264压缩技术的采用将大大节省用户的下载时间和数据流量收费。 +- 高质量的图象:H.264能提供连续、流畅的高质量图象(DVD质量)。 +- 容错能力强:H.264提供了解决在不稳定网络环境下容易发生的丢包等错误的必要工具。 +- 网络适应性强:H.264提供了网络适应层, 使得H.264的文件能容易地在不同网络上传输(例如互联网,CDMA,GPRS,WCDMA,CDMA2000等)。 + + + H.264和以前的标准一样,也是DPCM加变换编码的混合编码模式。但它采用“回归基本”的简洁设计,不用众多的选项,获得比H.263++好得多的压缩性能;加强了对各种信道的适应能力,采用“网络友好”的结构和语法,有利于对误码和丢包的处理;应用目标范围较宽,以满足不同速率、不同解析度以及不同传输(存储)场合的需求。 + +## H.264标准的关键技术 + + + +### 1 帧内预测编码 + + + 帧内编码用来缩减图像的空间冗余。为了提高H.264帧内编码的效率,在给定帧中充分利用相邻宏块的空间相关性,相邻的宏块通常含有相似的属性。因此,在对一给定宏块编码时,首先可以根据周围的宏块预测(典型的是根据左上角的宏块,因为此宏块已经被编码处理),然后对预测值与实际值的差值进行编码,这样,相对于直接对该帧编码而言,可以大大减小码率。 + + + +### 2帧间预测编码 + + + 帧间预测编码利用连续帧中的时间冗余来进行运动估计和补偿。H.264的运动补偿支持以往的视频编码标准中的大部分关键特性,而且灵活地添加了更多的功能,除了支持P帧、B帧外,H.264还支持一种新的流间传送帧——SP帧,如图3所示。码流中包含SP帧后,能在有类似内容但有不同码率的码流之间快速切换,同时支持随机接入和快速回放模式。 + + + +### 3整数变换 + + + 在变换方面,H.264使用了基于4×4像素块的类似于DCT的变换,但使用的是以整数为基础的空间变换,不存在反变换,因为取舍而存在误差的问题,变换矩阵如图5所示。与浮点运算相比,整数DCT变换会引起一些额外的误差,但因为DCT变换后的量化也存在量化误差,与之相比,整数DCT变换引起的量化误差影响并不大。此外,整数DCT变换还具有减少运算量和复杂度,有利于向定点DSP移植的优点。 + + + +### 4量化 + + + H.264中可选32种不同的量化步长,这与H.263中有31个量化步长很相似,但是在H.264中,步长是以12.5%的复合率递进的,而不是一个固定常数。 + + 在H.264中,变换系数的读出方式也有两种:之字形(Zigzag)扫描和双扫描,如图6所示。大多数情况下使用简单的之字形扫描;双扫描仅用于使用较小量化级的块内,有助于提高编码效率。 + + + +### 5熵编码 + + + 视频编码处理的最后一步就是熵编码,在H.264中采用了两种不同的熵编码方法:通用可变长编码(UVLC)和基于文本的自适应二进制算术编码(CABAC)。 + + 在H.263等标准中,根据要编码的数据类型如变换系数、运动矢量等,采用不同的VLC码表。H.264中的UVLC码表提供了一个简单的方法,不管符号表述什么类型的数据,都使用统一变字长编码表。其优点是简单;缺点是单一的码表是从概率统计分布模型得出的,没有考虑编码符号间的相关性,在中高码率时效果不是很好。 + + 因此,H.264中还提供了可选的CABAC方法。算术编码使编码和解码两边都能使用所有句法元素(变换系数、运动矢量)的概率模型。为了提高算术编码的效率,通过内容建模的过程,使基本概率模型能适应随视频帧而改变的统计特性。内容建模提供了编码符号的条件概率估计,利用合适的内容模型,存在于符号间的相关性可以通过选择目前要编码符号邻近的已编码符号的相应概率模型来去除,不同的句法元素通常保持不同的模型。 + +## H.264在实时视频聊天中的应用 + + + 目前,H.264已被广泛应用于实时视频应用中,相比以往的方案使得在同等速率下,H.264能够比H.263减小50%的码率。也就是说,用户即使是只利用 384kbit/s的带宽,就可以享受H.263下高达 768kbit/s的高质量视频服务。H.264 不但有助于节省庞大开支,还可以提高资源的使用效率,同时令达到商业质量的实时视频服务拥有更多的潜在客户。 + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file 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" new file mode 100644 index 00000000..45aa49ec --- /dev/null +++ "b/VideoDevelopment/\350\247\206\351\242\221\347\274\226\347\240\201/H264.md" @@ -0,0 +1,101 @@ +H264 +=== + + + + + +H.264是一种高性能的视频编解码技术。目前国际上制定视频编解码技术的组织有两个,一个是“国际电联”,它制定的标准有H.261、H.263、H.263+等,另一个是“国际标准化组织(ISO)”它制定的标准有MPEG-1、MPEG-2、MPEG-4等。而H.264则是由两个组织联合组建的联合视频组(JVT)共同制定的新数字视频编码标准,所以它既是ITU-T的H.264,又是ISO/IEC的MPEG-4高级视频编码,而且它将成为MPEG-4标准的第10部分。因此,不论是MPEG-4 AVC、MPEG-4 Part 10,还是ISO/IEC 14496-10,都是指H.264。 + + + + + +## H.264算法的优势 + + + H.264是在MPEG-4技术的基础之上建立起来的,其编解码流程主要包括5个部分:帧间和帧内预测、变换和反变换、量化和反量化、环路滤波、熵编码。 + + H.264/MPEG-4 AVC(H.264)是1995年自MPEG-2视频压缩标准发布以后的最新、最有前途的视频压缩标准。H.264是由ITU-T和ISO/IEC的联合开发组共同开发的最新国际视频编码标准。通过该标准,在同等图象质量下的压缩效率比以前的标准提高了2倍以上,因此,H.264被普遍认为是最有影响力的行业标准。 + +## H.264的优势 + + + H.264在1997年ITU的视频编码专家组提出时被称为H.26L,在ITU与ISO合作研究后被称为MPEG4 Part10或H.264(JVT)。H.264标准的主要目标是:与其它现有的视频编码标准相比,在相同的带宽下提供更加优秀的图象质量。 + +**而,H.264与以前的国际标准如H.263和MPEG-4相比,最大的优势体现在以下四个方面:** + + + +- 将每个视频帧分离成由像素组成的块,因此视频帧的编码处理的过程可以达到块的级别。 +- 采用空间冗余的方法,对视频帧的一些原始块进行空间预测、转换、优化和熵编码(可变长编码)。 +- 对连续帧的不同块采用临时存放的方法,这样,只需对连续帧中有改变的部分进行编码。该算法采用运动预测和运动补偿来完成。对某些特定的块,在一个或多个已经进行了编码的帧执行搜索来决定块的运动向量,并由此在后面的编码和解码中预测主块。 +- 采用剩余空间冗余技术,对视频帧里的残留块进行编码。例如:对于源块和相应预测块的不同,再次采用转换、优化和熵编码。 + + +**具体优势表现为:** + + + +- 低码流:和MPEG2和MPEG4 ASP等压缩技术相比,在同等图像质量下,采用H.264技术压缩后的数据量只有MPEG2的1/8,MPEG4的1/3。显然,H.264压缩技术的采用将大大节省用户的下载时间和数据流量收费。 +- 高质量的图象:H.264能提供连续、流畅的高质量图象(DVD质量)。 +- 容错能力强:H.264提供了解决在不稳定网络环境下容易发生的丢包等错误的必要工具。 +- 网络适应性强:H.264提供了网络适应层, 使得H.264的文件能容易地在不同网络上传输(例如互联网,CDMA,GPRS,WCDMA,CDMA2000等)。 + + + H.264和以前的标准一样,也是DPCM加变换编码的混合编码模式。但它采用“回归基本”的简洁设计,不用众多的选项,获得比H.263++好得多的压缩性能;加强了对各种信道的适应能力,采用“网络友好”的结构和语法,有利于对误码和丢包的处理;应用目标范围较宽,以满足不同速率、不同解析度以及不同传输(存储)场合的需求。 + +## H.264标准的关键技术 + + + +### 1 帧内预测编码 + + + 帧内编码用来缩减图像的空间冗余。为了提高H.264帧内编码的效率,在给定帧中充分利用相邻宏块的空间相关性,相邻的宏块通常含有相似的属性。因此,在对一给定宏块编码时,首先可以根据周围的宏块预测(典型的是根据左上角的宏块,因为此宏块已经被编码处理),然后对预测值与实际值的差值进行编码,这样,相对于直接对该帧编码而言,可以大大减小码率。 + + + +### 2帧间预测编码 + + + 帧间预测编码利用连续帧中的时间冗余来进行运动估计和补偿。H.264的运动补偿支持以往的视频编码标准中的大部分关键特性,而且灵活地添加了更多的功能,除了支持P帧、B帧外,H.264还支持一种新的流间传送帧——SP帧,如图3所示。码流中包含SP帧后,能在有类似内容但有不同码率的码流之间快速切换,同时支持随机接入和快速回放模式。 + + + +### 3整数变换 + + + 在变换方面,H.264使用了基于4×4像素块的类似于DCT的变换,但使用的是以整数为基础的空间变换,不存在反变换,因为取舍而存在误差的问题,变换矩阵如图5所示。与浮点运算相比,整数DCT变换会引起一些额外的误差,但因为DCT变换后的量化也存在量化误差,与之相比,整数DCT变换引起的量化误差影响并不大。此外,整数DCT变换还具有减少运算量和复杂度,有利于向定点DSP移植的优点。 + + + +### 4量化 + + + H.264中可选32种不同的量化步长,这与H.263中有31个量化步长很相似,但是在H.264中,步长是以12.5%的复合率递进的,而不是一个固定常数。 + + 在H.264中,变换系数的读出方式也有两种:之字形(Zigzag)扫描和双扫描,如图6所示。大多数情况下使用简单的之字形扫描;双扫描仅用于使用较小量化级的块内,有助于提高编码效率。 + + + +### 5熵编码 + + + 视频编码处理的最后一步就是熵编码,在H.264中采用了两种不同的熵编码方法:通用可变长编码(UVLC)和基于文本的自适应二进制算术编码(CABAC)。 + + 在H.263等标准中,根据要编码的数据类型如变换系数、运动矢量等,采用不同的VLC码表。H.264中的UVLC码表提供了一个简单的方法,不管符号表述什么类型的数据,都使用统一变字长编码表。其优点是简单;缺点是单一的码表是从概率统计分布模型得出的,没有考虑编码符号间的相关性,在中高码率时效果不是很好。 + + 因此,H.264中还提供了可选的CABAC方法。算术编码使编码和解码两边都能使用所有句法元素(变换系数、运动矢量)的概率模型。为了提高算术编码的效率,通过内容建模的过程,使基本概率模型能适应随视频帧而改变的统计特性。内容建模提供了编码符号的条件概率估计,利用合适的内容模型,存在于符号间的相关性可以通过选择目前要编码符号邻近的已编码符号的相应概率模型来去除,不同的句法元素通常保持不同的模型。 + +## H.264在实时视频聊天中的应用 + + + 目前,H.264已被广泛应用于实时视频应用中,相比以往的方案使得在同等速率下,H.264能够比H.263减小50%的码率。也就是说,用户即使是只利用 384kbit/s的带宽,就可以享受H.263下高达 768kbit/s的高质量视频服务。H.264 不但有助于节省庞大开支,还可以提高资源的使用效率,同时令达到商业质量的实时视频服务拥有更多的潜在客户。 + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/VideoDevelopment/\350\247\206\351\242\221\347\274\226\347\240\201/H265.md" "b/VideoDevelopment/\350\247\206\351\242\221\347\274\226\347\240\201/H265.md" new file mode 100644 index 00000000..45aa49ec --- /dev/null +++ "b/VideoDevelopment/\350\247\206\351\242\221\347\274\226\347\240\201/H265.md" @@ -0,0 +1,101 @@ +H264 +=== + + + + + +H.264是一种高性能的视频编解码技术。目前国际上制定视频编解码技术的组织有两个,一个是“国际电联”,它制定的标准有H.261、H.263、H.263+等,另一个是“国际标准化组织(ISO)”它制定的标准有MPEG-1、MPEG-2、MPEG-4等。而H.264则是由两个组织联合组建的联合视频组(JVT)共同制定的新数字视频编码标准,所以它既是ITU-T的H.264,又是ISO/IEC的MPEG-4高级视频编码,而且它将成为MPEG-4标准的第10部分。因此,不论是MPEG-4 AVC、MPEG-4 Part 10,还是ISO/IEC 14496-10,都是指H.264。 + + + + + +## H.264算法的优势 + + + H.264是在MPEG-4技术的基础之上建立起来的,其编解码流程主要包括5个部分:帧间和帧内预测、变换和反变换、量化和反量化、环路滤波、熵编码。 + + H.264/MPEG-4 AVC(H.264)是1995年自MPEG-2视频压缩标准发布以后的最新、最有前途的视频压缩标准。H.264是由ITU-T和ISO/IEC的联合开发组共同开发的最新国际视频编码标准。通过该标准,在同等图象质量下的压缩效率比以前的标准提高了2倍以上,因此,H.264被普遍认为是最有影响力的行业标准。 + +## H.264的优势 + + + H.264在1997年ITU的视频编码专家组提出时被称为H.26L,在ITU与ISO合作研究后被称为MPEG4 Part10或H.264(JVT)。H.264标准的主要目标是:与其它现有的视频编码标准相比,在相同的带宽下提供更加优秀的图象质量。 + +**而,H.264与以前的国际标准如H.263和MPEG-4相比,最大的优势体现在以下四个方面:** + + + +- 将每个视频帧分离成由像素组成的块,因此视频帧的编码处理的过程可以达到块的级别。 +- 采用空间冗余的方法,对视频帧的一些原始块进行空间预测、转换、优化和熵编码(可变长编码)。 +- 对连续帧的不同块采用临时存放的方法,这样,只需对连续帧中有改变的部分进行编码。该算法采用运动预测和运动补偿来完成。对某些特定的块,在一个或多个已经进行了编码的帧执行搜索来决定块的运动向量,并由此在后面的编码和解码中预测主块。 +- 采用剩余空间冗余技术,对视频帧里的残留块进行编码。例如:对于源块和相应预测块的不同,再次采用转换、优化和熵编码。 + + +**具体优势表现为:** + + + +- 低码流:和MPEG2和MPEG4 ASP等压缩技术相比,在同等图像质量下,采用H.264技术压缩后的数据量只有MPEG2的1/8,MPEG4的1/3。显然,H.264压缩技术的采用将大大节省用户的下载时间和数据流量收费。 +- 高质量的图象:H.264能提供连续、流畅的高质量图象(DVD质量)。 +- 容错能力强:H.264提供了解决在不稳定网络环境下容易发生的丢包等错误的必要工具。 +- 网络适应性强:H.264提供了网络适应层, 使得H.264的文件能容易地在不同网络上传输(例如互联网,CDMA,GPRS,WCDMA,CDMA2000等)。 + + + H.264和以前的标准一样,也是DPCM加变换编码的混合编码模式。但它采用“回归基本”的简洁设计,不用众多的选项,获得比H.263++好得多的压缩性能;加强了对各种信道的适应能力,采用“网络友好”的结构和语法,有利于对误码和丢包的处理;应用目标范围较宽,以满足不同速率、不同解析度以及不同传输(存储)场合的需求。 + +## H.264标准的关键技术 + + + +### 1 帧内预测编码 + + + 帧内编码用来缩减图像的空间冗余。为了提高H.264帧内编码的效率,在给定帧中充分利用相邻宏块的空间相关性,相邻的宏块通常含有相似的属性。因此,在对一给定宏块编码时,首先可以根据周围的宏块预测(典型的是根据左上角的宏块,因为此宏块已经被编码处理),然后对预测值与实际值的差值进行编码,这样,相对于直接对该帧编码而言,可以大大减小码率。 + + + +### 2帧间预测编码 + + + 帧间预测编码利用连续帧中的时间冗余来进行运动估计和补偿。H.264的运动补偿支持以往的视频编码标准中的大部分关键特性,而且灵活地添加了更多的功能,除了支持P帧、B帧外,H.264还支持一种新的流间传送帧——SP帧,如图3所示。码流中包含SP帧后,能在有类似内容但有不同码率的码流之间快速切换,同时支持随机接入和快速回放模式。 + + + +### 3整数变换 + + + 在变换方面,H.264使用了基于4×4像素块的类似于DCT的变换,但使用的是以整数为基础的空间变换,不存在反变换,因为取舍而存在误差的问题,变换矩阵如图5所示。与浮点运算相比,整数DCT变换会引起一些额外的误差,但因为DCT变换后的量化也存在量化误差,与之相比,整数DCT变换引起的量化误差影响并不大。此外,整数DCT变换还具有减少运算量和复杂度,有利于向定点DSP移植的优点。 + + + +### 4量化 + + + H.264中可选32种不同的量化步长,这与H.263中有31个量化步长很相似,但是在H.264中,步长是以12.5%的复合率递进的,而不是一个固定常数。 + + 在H.264中,变换系数的读出方式也有两种:之字形(Zigzag)扫描和双扫描,如图6所示。大多数情况下使用简单的之字形扫描;双扫描仅用于使用较小量化级的块内,有助于提高编码效率。 + + + +### 5熵编码 + + + 视频编码处理的最后一步就是熵编码,在H.264中采用了两种不同的熵编码方法:通用可变长编码(UVLC)和基于文本的自适应二进制算术编码(CABAC)。 + + 在H.263等标准中,根据要编码的数据类型如变换系数、运动矢量等,采用不同的VLC码表。H.264中的UVLC码表提供了一个简单的方法,不管符号表述什么类型的数据,都使用统一变字长编码表。其优点是简单;缺点是单一的码表是从概率统计分布模型得出的,没有考虑编码符号间的相关性,在中高码率时效果不是很好。 + + 因此,H.264中还提供了可选的CABAC方法。算术编码使编码和解码两边都能使用所有句法元素(变换系数、运动矢量)的概率模型。为了提高算术编码的效率,通过内容建模的过程,使基本概率模型能适应随视频帧而改变的统计特性。内容建模提供了编码符号的条件概率估计,利用合适的内容模型,存在于符号间的相关性可以通过选择目前要编码符号邻近的已编码符号的相应概率模型来去除,不同的句法元素通常保持不同的模型。 + +## H.264在实时视频聊天中的应用 + + + 目前,H.264已被广泛应用于实时视频应用中,相比以往的方案使得在同等速率下,H.264能够比H.263减小50%的码率。也就是说,用户即使是只利用 384kbit/s的带宽,就可以享受H.263下高达 768kbit/s的高质量视频服务。H.264 不但有助于节省庞大开支,还可以提高资源的使用效率,同时令达到商业质量的实时视频服务拥有更多的潜在客户。 + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/VideoDevelopment/\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" "b/VideoDevelopment/\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" index a3329194..a6e48d63 100644 --- "a/VideoDevelopment/\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" +++ "b/VideoDevelopment/\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" @@ -209,6 +209,10 @@ PAR DAR SAR 音视频在网络上传播的时候,常常采用各种流媒体协议,例如HTTP、RTMP等等,这些协议在传输音视频数据的同时也会传输一些信令数据。这些信令数据包括对播放的控制(播放、暂停、停止)或者对网络状态的描述等。解协议的过程中会去除掉信令数据而只保留音视频数据。例如,采用RTMP协议传输的数据,经过解协议操作后,输出FLV格式的数据。 +[常见流媒体协议](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) + + + From 5a9fcd1d9a7bd8bab7645015455612a15467a33a Mon Sep 17 00:00:00 2001 From: CharonChui Date: Sat, 6 Jun 2020 21:00:28 +0800 Subject: [PATCH 021/213] add images --- .../DASH.md" | 86 +---- .../HLS.md" | 27 +- .../HTTP FLV.md" | 91 ++++++ .../RTMP.md" | 31 ++ ...32\344\277\241\345\215\217\350\256\256.md" | 299 ++++++++++++++++-- .../FLV.md" | 21 ++ ...74\345\274\217\350\257\246\350\247\243.md" | 63 +++- .../TS.md" | 94 ++++++ .../fMP4 vs ts.md" | 98 ++++++ ...74\345\274\217\350\257\246\350\247\243.md" | 58 ++++ ...01\350\243\205\346\240\274\345\274\217.md" | 195 ++++++++++++ 11 files changed, 947 insertions(+), 116 deletions(-) create mode 100644 "VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/HTTP FLV.md" create mode 100644 "VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/RTMP.md" create mode 100644 "VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/FLV.md" rename "VideoDevelopment/MP4\346\240\274\345\274\217\350\257\246\350\247\243.md" => "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" (57%) create mode 100644 "VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/TS.md" create mode 100644 "VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/fMP4 vs ts.md" create mode 100644 "VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/fMP4\346\240\274\345\274\217\350\257\246\350\247\243.md" create mode 100644 "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" diff --git "a/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/DASH.md" "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/DASH.md" index 0ebf8bd3..21743f4a 100644 --- "a/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/DASH.md" +++ "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/DASH.md" @@ -2,7 +2,9 @@ 除了前面讲的Apple的HLS,还有Adobe HTTP Dynamic Streaming (HDS)、Microsoft Smooth Streaming (MSS)。他们各家的协议原理大致相同,但是格式又不一样,也无法兼容,所以Moving Picture Expert Group (MPEG) 就把大家叫到了一起,呼吁大家一起来制定一个标准的,然后就有了[MPEG-DASH](https://www.encoding.com/mpeg-dash/),它的主要目标是形成IP网络承载单一格式的流媒体并提供高效与高质量服务的统一方案,解决多制式传输方案(HTTP Live Streaming, Microsoft Smooth Streaming, HTTP Dynamic Streaming)并存格局下的存储与服务能力浪费、运营高成本与复杂度、系统间互操作弱等问题。 -[DASH(MPEG-DASH)](https://mpeg.chiariglione.org/standards/mpeg-dash/)全称为Dynamic Adaptive Streaming over HTTP.是由MPEG和ISO批准的独立于供应商的国际标准,它是一种基于HTTP的使用TCP传输协议的流媒体传输技术。MPEG-DASH是一种自适应比特率流技术,可根据实时网络状况实现动态自适应下载。和HLS, HDS技术类似, 都是把视频分割成一小段一小段, 通过HTTP协议进行传输,客户端得到之后进行播放;不同的是MPEG-DASH支持MPEG-2 TS、MP4(最新的HLS也支持了MP4)等多种格式, 可以将视频按照多种编码切割, 下载下来的媒体格式既可以是ts文件也可以是mp4文件,MPEG—DASH技术与编解码器无关,可使用H.265,H.264,VP9等任何编解码器进行编码。 +[DASH(MPEG-DASH)](https://mpeg.chiariglione.org/standards/mpeg-dash/)全称为Dynamic Adaptive Streaming over HTTP.是由MPEG和ISO批准的独立于供应商的国际标准,它是一种基于HTTP的使用TCP传输协议的流媒体传输技术。它诞生的目的是为了统一标准,因此是兼容SmoothStreaming和HLS的.同时支持TS profile和 ISO profile,支持节目观看等级控制,支持父母锁. mpeg dash支持的DRM类型包括PlayReady和Marlin,而HLS支持的是AES128(密钥长度为128位的高级加密标准Advanced Encryption Standard)加密类型。 + +MPEG-DASH是一种自适应比特率流技术,可根据实时网络状况实现动态自适应下载。和HLS, HDS技术类似, 都是把视频分割成一小段一小段, 通过HTTP协议进行传输,客户端得到之后进行播放;不同的是MPEG-DASH支持MPEG-2 TS、MP4(最新的HLS也支持了MP4)等多种格式, 可以将视频按照多种编码切割, 下载下来的媒体格式既可以是ts文件也可以是mp4文件,MPEG—DASH技术与编解码器无关,可使用H.265,H.264,VP9等任何编解码器进行编码。 安卓平台上的ExoPlayer支持MPEG-DASH。另外,三星、索尼、飞利浦、松下的一些较新型号的智能电视支持MPEG—DASH。Google的Chromecast、YouTube 和Netflix 也已支持MPEG-DASH。 @@ -123,71 +125,21 @@ Segments可以包含任何媒体数据,关于容器,官方提供了两种建 -## fMP4 - -[MP4和fMP4的详细介绍](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/MP4%E6%A0%BC%E5%BC%8F%E8%AF%A6%E8%A7%A3.md) - - - -fMP4(fragmented MP4),可以简单理解为分片化的MP4,是DASH采用的媒体文件格式,文件扩展名通常为(.m4s或直接用.mp4)。 - -![fMP4](https://img-blog.csdn.net/20171107114807709?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveXVlX2h1YW5n/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) - - - -### fMP4与ts的区别 - -最主要的就是.ts文件不提供关于时长等信息,你无法在ts文件中去实现音视频的seek操作。fmp4不同于ts,它是提供了时长等信息,可以执行seek到指定位置。 - - - -### 媒体数据与元数据的分离 - -在mp4格式中,元数据可以和媒体数据很好地分开存储,后者都在mdat box中,而在ts中,诸多es流和header/metadata信息是复用在一起的。 - 元数据的分离允许我们在streaming中先读取各个流的元数据,知道他们的媒体内容的属性(比如不同的视频质量、不同的语言等),从而可以更好地在不同的media data之间做自适应切换。 - 当然,在更实际的应用场景中,比如在dash协议中会直接把这些元数据信息写在mpd中,player可以只读一个mpd就知道各个媒体数据的属性 - -### 各个Track独立存储 - -在fmp4中,不仅媒体数据和metadata相互独立的存储,音视频track的数据也可以分开存储,这里的“分开”已经不仅仅局限于box层面的分开,而是真的可以分开存储于不同的目录。在这种情况下,player只需要读一个记录了它们各自存储位置的manifest,即可去对应的位置download它们的分片,只要做好音视频分片之间的同步工作,就可以正常的播放。 - 举个例子,下面是一个dash中常见的mpd: - -在这个mpd中我们看到视频的分片都是存储在video/1/这个目录下,而音频分片都存储在audio/und/mp4a/1这个目录下,而player还是可以将它们拼接到一起完成播放。 - 相对地,在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)文件可以更加简洁,如下: -针对不同语言的音频,都只需要存一个大文件就够了。 -相对地,在streaming TS流时,不得不在manifest(m3u8)文件中把成百上千个ts分片文件全都老老实实地记录下来。 -所以Dash同样可以支持下面的操作: -- 音频视频分离,在后台播放时可以只拉取音频 -- 支持多音轨,多视频轨,多字幕任意切换 -### 服务器的cache效率会降低 - 实际的streaming应用场景中,往往需要cdn的支持,经常会被client请求的媒体分片就会存在距离client最近的edge server上。对于fmp4 streaming的情况,因为需要的文件更少,cache命中率也就更高,举个例子:可能某一个audio track会和其他各种video track组合,那么就可以将这个audio track放在edge server上,而不用每次都跟origin server去请求。 - 相对地,在streaming TS流时,因为每一个音视频组合的都需要以复用文件的形式存储,组合数又非常多,相当于分母大了,edge server就会有很大的几率没有缓存需要的组合而要去向orgin server请求。 +### HLS vs DASH -### 对Trick-play的支持 -所谓Trick-play,就是快进、快退、直接跳到章节起点、慢动作播放这些“花式”播放功能。支持这些功能往往意味着要快速找到播放流中的关键帧,以快进播放为例,如果利用fmp4格式的特点,可以通过只读取每个媒体分片的moof加上mdat的起始(包含了关键帧图像)部分即可,说白了就是通过只显示关键帧的方法达到“快进”的视觉效果。因为fmp4格式中可以保证每一个分片一定是以IDR帧开始的,这就使得上述的方案实现起来非常方便。 - 相对地,在streaming TS流时,没有办法保证关键帧一定在什么位置,所以你可能需要解析一大堆TS packets才能找到关键帧的位置。 -### 无缝码流切换 +- 在标准HTTP服务器上的用法: HLS和DASH均可在常规HTTP服务器(例如Nginx,Apache等)上使用。 +- 多个音频通道: 特别是对于多语言内容,重要的是能够在各个语言的不同音频通道之间进行切换。 DASH和HLS都可以做到这一点。 +- 字幕和标题: 为了给视频添加字幕,通常创建一个单独的文件,例如,文件可以具有WebVTT格式。然后从清单(即.m3u8或.mpd文件)中引用该文件。 +- 插入广告: 通常,可以在HLS和DASH的实时流中插入广告。为此,只需交换单个视频块。 DASH为此提供了一种有效的方法:标准化的界面允许有效地插入广告。 +- 快速频道切换: 您可以在各个通道之间切换的速度取决于最大的子段(块)。块越小,通道更改速度越快。正如引言中已经提到的,HLS块通常长约10秒,而DASH块通常长2至4秒。因此,DASH在这方面领先一步。小块还具有降低代码效率的缺点。具有较小块的播放列表必须比具有较大块的播放列表更频繁地更新。这意味着包含较短视频片段的播放列表必须通过HTTP更频繁地更新。 -无缝码流切换实现的关键在于:当第一个码流播放结束时,也就是发生切换的时间,第二个码流一定要以关键帧开始播放。在streaming TS流时,因为不能保证每一个TS chunk一定以关键帧开始,做码流切换时就意味着要同时download两个码流的相应分片,同时解析两个码流,然后找到关键帧对应的位置,才能切换。同时下载、解析两个码流的媒体内容对网络带宽以及设备性能都形成了挑战。而且有意思的是,如果当前网络环境不佳,player想要切换到低码率码流,结果还要在本来就不好的网络环境下同时进行两个码流的下载,可谓是雪上加霜。 - 而在fmp4中,除了保证各个分片一定以IDR帧开始外,还能保证不同码流的分片之间在时间线上是对齐的。而且streaming fmp4流时因为不要求音视频复用存储,也就意味着视频和音频的同步点可以不一样,视频可以全都以GOP边界作为同步点,音频可以都以sync frame作为同步点,这都使得无缝码流切换更简单。 -### 与DRM的集成 - -所谓DRM即数字版权管理,说白了就是对流进行加密,这东西在国内用的不多,但是在国外可是每一个内容提供商必须要有的东西。和编码标准一样,业界也存在很多DRM方案,为了避免每采用一个新的加密方案就要重新编一个码流,MPEG推出了通用加密(CENC)标准(23001-7 - Common Encryption)。使用这一标准的码流,就可以将一个码流应用于各种不同的DRM方案。在DASH spec中,也定义了Content Protection字段来对应这种加密方案。 - CENC使用的就是fMP4格式,这是利用了fMP4中音视频可以不复用同时还能提供独立于media data存储的metadata的特点。TS流就享受不了这样的好处了。 - -DASH和HLS之间的另一个关键区别是它支持DRM。可是,在DASH中不存在一个单一通用的DRM解决方案。例如,Google的Chrome支持Widevine,而Microsoft的Internet Explorer支持PlayReady。然而,通过使用MPEG-CENC(MPEG通用加密)结合加密媒体扩展(EME),视频流内容可以仅被加密一次。HLS支持AES-128加密,以及苹果自己的DRM,Fairplay。 @@ -195,8 +147,6 @@ DASH和HLS之间的另一个关键区别是它支持DRM。可是,在DASH中不 - HEVC HLS with fMP4: [http://bitmovin-a.akamaihd.net/content/dataset/multi-codec/hevc/stream_fmp4.m3u8](https://bitmovin-a.akamaihd.net/content/dataset/multi-codec/hevc/stream_fmp4.m3u8) - - - HEVC HLS with TS (not supported by Apple): [http://bitmovin-a.akamaihd.net/content/dataset/multi-codec/hevc/stream_ts.m3u8](https://bitmovin-a.akamaihd.net/content/dataset/multi-codec/hevc/stream_ts.m3u8) [https://bitmovin-a.akamaihd.net/content/playhouse-vr/m3u8s/105560.m3u8](https://bitmovin-a.akamaihd.net/content/playhouse-vr/m3u8s/105560.m3u8) @@ -213,23 +163,11 @@ DASH和HLS之间的另一个关键区别是它支持DRM。可是,在DASH中不 参考: -[HLS,MPEG-DASH - What is ABR?](http://telestreamblog.telestream.net/2017/05/what-is-abr/) - -[B站我们为什么使用DASH](https://www.bilibili.com/read/cv855111) - - - - - - - - - - - - +- [HLS,MPEG-DASH - What is ABR?](http://telestreamblog.telestream.net/2017/05/what-is-abr/) +- [B站我们为什么使用DASH](https://www.bilibili.com/read/cv855111) +- [Adaptive HTTP Streaming Technologies: HLS vs. DASH](https://strivecast.io/hls-vs-mpeg-dash/) 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 3aede2fa..acfeaf00 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" @@ -11,11 +11,23 @@ HLS协议规定: HLS协议由三部分组成: - HTTP:传输协议 -- m3u8:索引文件 +- m3u8:索引文件(m3u8 文件本质说其实是采用了编码是 UTF-8 的 m3u 文件。) - TS:音视频媒体信息 HLS点播,基本上就是常见的分段HTTP点播,不同在于,它的分段非常小。相对于常见的流媒体直播协议,例如RTMP协议、RTSP协议、MMS协议等,HLS直播最大的不同在于,直播客户端获取到的,并不是一个完整的数据流。HLS协议在服务器端将直播数据流存储为连续的、很短时长的媒体文件(MPEG-TS格式),而客户端则不断的下载并播放这些小文件。因为服务器端总是会将最新的直播数据生成新的小文件,这样客户端只要不停的按顺序播放从服务器获取到的文件,就实现了直播。由此可见,基本上可以认为,HLS是以点播的技术方式来实现直播。由于数据通过HTTP协议传输,所以完全不用考虑防火墙或者代理的问题,而且分段文件的时长很短,客户端可以很快的选择和切换码率,以适应不同带宽条件下的播放。不过HLS的这种技术特点,决定了它的延迟一般总是会高于普通的流媒体直播协议。它也解决了RTMP协议存在的一些问题,例如RTMP协议不使用标准的HTTP接口传输数据(TCP、UDP端口),所以在一些特殊的网络环境下可能被防火墙屏蔽掉,而HLS使用的是HTTP协议传输数据(80端口),不会遇到被防火墙屏蔽的情况。 + + +### HLS架构 + +HLS的架构分为三部分:Server,CDN,Client 。即服务器、分发组件和客户端。 + +下面是 HLS 整体架构图: + + + + + ## TS TS(Transport Stream),全程为MPEG2-TS。它的特点就是要求从视频流的任一片段开始都是可以独立解码的。DVD节目中的MPEG2格式,确切的说是MPEG2-PS,全程是Program Stream,而TS的全称是Transport Stream。MPEG2-PS主要应用于存储的具有固定市场的节目,如DVD电影,而MPEG2-TS则主要应用于实时传送的节目,比如实时广播的电视节目。这两种格式的主要区别是什么呢?简单地打个比喻说,你将DVD上的[VOB](https://baike.baidu.com/item/VOB)文件的前面一截cut掉(或者干脆就是数据损坏),那么就会导致整个文件无法解码了,而电视节目是你任何时候打开电视机都能解码(收看)的,所以,MPEG2-TS格式的特点就是要求从[视频流](https://baike.baidu.com/item/视频流)的任一片段开始都是可以独立解码的。 @@ -27,7 +39,7 @@ TS(Transport Stream),全程为MPEG2-TS。它的特点就是要求从视频流 - 是一个索引地址/播放列表,通过FFmpeg将本地的xxx.mp4进行切片处理,生成m3u8播放列表(索引文件)和N多个 .ts文件,并将其(m3u8、N个ts)放置在本地搭建好的webServer服务器的指定目录下,我就可以得到一个可以实时播放的网址,我们把这个m3u8地址复制到 VLC 上就可以实时观看! 在 HLS 流下,本地视频被分割成一个一个的小切片,一般10秒一个,这些个小切片被 m3u8管理,并且随着终端的FFmpeg 向本地拉流的命令而实时更新,影片进度随着拉流的进度而更新,播放过的片段不在本地保存,自动删除,直到该文件播放完毕或停止,ts 切片会相应的被删除,流停止,影片不会立即停止,影片播放会滞后于拉流一段时间 - + - Media encoder(媒体编码) @@ -138,16 +150,19 @@ cctv1hd-1585920074000.ts ## HLS 的劣势 -- 相比 RTMP 这类长连接协议, 延时较高, 难以用到互动直播场景.HLS 理论延时 = 1 个切片的时长 + 0-1个 td (td 是 EXT-X-TARGETDURATION, 可简单理解为播放器取片的间隔时间) + 0-n 个启动切片(苹果官方建议是请求到 3 个片之后才开始播放) + 播放器最开始请求的片的网络延时(网络连接耗时)。为了追求低延时效果, 可以将切片切的更小, 取片间隔做的更小, 播放器未取到 3 个片就启动播放. 但是, 这些优化方式都会增加 HLS 不稳定和出现错误的风险. +- 相比 RTMP 这类长连接协议, 延时较高, 难以用到互动直播场景.HLS 理论延时 = 1 个切片的时长 + 0-1个 td (td 是 EXT-X-TARGETDURATION, 可简单理解为播放器取片的间隔时间) + 0-n 个启动切片(苹果官方建议是请求到 3 个片之后才开始播放) + 播放器最开始请求的片的网络延时(网络连接耗时)。为了追求低延时效果, 可以将切片切的更小, 取片间隔做的更小, 播放器未取到 3 个片就启动播放. 但是, 这些优化方式都会增加 HLS 不稳定和出现错误的风险.通常 HLS 直播延时会达到 20-30s,而高延时对于需要实时互动体验的直播来说是不可接受的。 - 对于点播服务来说, 由于 TS 切片通常较小, 海量碎片在文件分发, 一致性缓存, 存储等方面都有较大挑战. +- HLS 基于短连接 HTTP,HTTP 是基于 TCP 的,这就意味着 HLS 需要不断地与服务器建立连接,TCP 每次建立连接时的三次握手、慢启动过程、断开连接时的四次挥手都会产生消耗。 - - - +CCTV1高清:http://ivi.bupt.edu.cn/hls/cctv1hd.m3u8 +CCTV3高清:http://ivi.bupt.edu.cn/hls/cctv3hd.m3u8 +CCTV5高清:http://ivi.bupt.edu.cn/hls/cctv5hd.m3u8 +CCTV5+高清:http://ivi.bupt.edu.cn/hls/cctv5phd.m3u8 +CCTV6高清:http://ivi.bupt.edu.cn/hls/cctv6hd.m3u8 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" new file mode 100644 index 00000000..2ac14110 --- /dev/null +++ "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/HTTP FLV.md" @@ -0,0 +1,91 @@ +HTTP FLV +=== + +先看看HTTP-FLV长成什么样子:http://ip:port/live/livestream.flv,协议头是http,另外”.flv”这个尾巴是它最明显的特征。 + +HttpFlv 就是 http+flv ,将音视频数据封装成FLV格式,然后通过 HTTP 协议传输给客户端。 + + + +下的直播平台中大部分的主线路使用的都是HTTP-FLV协议,备线路多为RTMP。小编随便在Safari中打开几个直播平台房间,一抓包就不难发现使用HTTP-FLV协议的身影:熊猫、斗鱼、虎牙、B站。 + + + + + +FLV(Flash Video)是Adobe公司设计开发的一种流行的流媒体格式,由于其视频文件体积轻巧、封装简单等特点,使其很适合在互联网上进行应用。此外,FLV可以使用Flash Player进行播放,而Flash Player插件已经安装在绝大部分浏览器上,这使得通过网页播放FLV视频十分容易。FLV封装格式的文件后缀通常为“.flv”。 + +在说HTTP-FLV之前,我们有必要对FLV adobe 官方标准有个认识,因为HTTP-FLV协议中封装格式使用的是FLV。 + +FLV文件格式标准是写F4V/FLV fileformat spec v10.1的附录E里面的FLVFile Format。 + + + +Flash Video`:是Adobe公司推出的另一种视频格式,是一种在网络上传输的流媒体数据存储容器格式。其格式相对简单轻量,不需要很大的媒体头部信息。整个FLV由Header和Body以及其他Tag组成。因此加载速度极快。它是基于HTTP/80传输,可以避免被防火墙拦截的问题,除此之外,它可以通过 HTTP 302 跳转灵活调度/负载均衡,支持使用 HTTPS 加密传输,也能够兼容支持 Android,iOS 的移动端。但是由于它的传输特性,会让流媒体资源缓存在本地客户端,在保密性方面不够好,因为网络流量较大,它也不适合做拉流协议。 + + + + + +HTTP-FLV + +我们这里说的HTTP-FLV,主要是说的是HTTP-FLV流,而不是基于HTTP的FLV视频文件点播,也不是能够随意SEEK的HTTP FLV伪流。 + +FLV渐进式下载:通过HTTP协议将FLV下载到播放器中播放,无法直接拉到中间去播放。 + +HTTP FLV伪流:支持SEEK,可从未下载的部分开始播放。 + +HTTP-FLV流:拥有和流式协议RTMP一样的特征,长连接,流式数据。 + + + +▣ HTTP-FLV技术实现 + +HTTP协议中有个content-length字段的约定,即http的body部分的长度。服务器回复http请求时如果有这个字段,客户端就接收这个长度的数据然后认为数据传输完成了,开始播放。 + +如果服务器回复http请求中没有这个字段,客户端就一直保持长连接接收数据,直到服务器跟客户端的socket断开。 + +HTTP-FLV流就利用了上述的第二个原理,服务器回复客户端请求的时候不加content-length字段,在回复了http内容之后,进行持续的数据发送,客户端就一直接收数据,以此实现了HTTP-FLV流直播。 + +数据传输依然是之前讲过的内容,每一个音视频数据都被封装成包含时间戳信息头的数据包,封装格式采用FLV,传输协议采用http。 + + + + + +❸ RTMP和HTTP-FLV的比较: + +RTMP和HTTP-FLV延迟上保持一致,在这二者的区别如下: + +▲ 穿墙:很多防火墙会墙掉RTMP,但是不会墙HTTP,因此HTTPFLV更不容易出问题。 + +▲ 调度:虽然RTMP也能支持302,单实现起来较麻烦。HTTP FLV本身就支持302,更方便CDN进行重定向以便更精准的调度。 + +▲ 容错: HTTP-FLV回源时也可以回多个源,能做到和RTMP一样,支持多级热备。 + +▲ 简单:FLV是最简单的流媒体封装,HTTP是最广泛的协议,这两个组合在一起维护性更高,比RTMP简单。 + +▲ 友好:HTTP-FLV代码量更小,集成SDK也更轻便。使用起来也更简单。 + +综上,HTTP-FLV协议具有RTMP的延迟优势,又继承了HTTP所有优势,是流媒体直播首选的分发协议。 + + + + + + + + + +https://blog.csdn.net/luzubodfgs/article/details/78155117 + + + +https://blog.csdn.net/qq_37382077/article/details/103386289 + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/RTMP.md" "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/RTMP.md" new file mode 100644 index 00000000..e3918a9d --- /dev/null +++ "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/RTMP.md" @@ -0,0 +1,31 @@ +RTMP +=== + + + + + +http://billchan.me/2019/04/27/livestreamprotocol/ + + + +Real Time Messaging Protocol(实时消息传送协议):是Adobe Systems公司为`Flash`播放器和服务器之间音频、视频和数据传输开发的开放协议。协议基于`TCP`,是一个协议族(默认端口1935),包括`RTMP`基本协议及`RTMPT/RTMPS/RTMPE`等多种变种。`RTMP` 是一种设计用来进行实时数据通信的网络协议,主要用来在`Flash/AIR`平台和支持`RTMP`协议的流媒体/交互服务器之间进行音视频和数据通信。市面上绝大部分PC秀场使用的都是它,他有低延迟(2s左右)、稳定性高、技术完善、高支持度、编码兼容性高等特点。但是RTMP协议不使用标准的HTTP接口传输数据(TCP、UDP端口),所以在一些特殊的网络环境下可能被防火墙屏蔽掉。 + +RTMP的整体流程为: + +视频采集器 -> 支持RTMP的视频编码器 -> 网络传输 -> 流媒体服务器 -> 网络 -> 客户端 + + + + + + + +https://blog.csdn.net/qq_37382077/article/details/103386289 + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/\346\265\201\345\252\222\344\275\223\351\200\232\344\277\241\345\215\217\350\256\256.md" "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/\346\265\201\345\252\222\344\275\223\351\200\232\344\277\241\345\215\217\350\256\256.md" index 5eb94854..402a87e7 100644 --- "a/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/\346\265\201\345\252\222\344\275\223\351\200\232\344\277\241\345\215\217\350\256\256.md" +++ "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/\346\265\201\345\252\222\344\275\223\351\200\232\344\277\241\345\215\217\350\256\256.md" @@ -1,81 +1,298 @@ 流媒体通信协议 === -流媒体(Streaming Media)是指采用流式传输技术在网络上连续实时播放的媒体格式,如音频、视频或多媒体文件,采用流媒体技术使得数据包得以像流水一样发送, 如果没有流媒体技术, 那么我们就要像以前用迅雷下电影一样, 下载整个影片才能观看。 +## 流媒体 -HTTP ---- +流媒体(Streaming Media)又叫流式媒体。是指采用流式传输技术在网络上连续实时播放的媒体格式,如音频、视频或多媒体文件,采用流媒体技术使得数据包得以像流水一样发送, 如果没有流媒体技术, 那么我们就要像以前用迅雷下电影一样, 下载整个影片才能观看。这样我们需要在漫长的等待之后(因为受限于带宽,下载通常要花上较长的时间),才可以看到或听到媒体传达的信息。令人欣慰的是,在流媒体技术出现之后,人们便无需再等待媒体完全下载完成了。 -HTTP 视频协议是在互联网普及之后在互联网上看视频的需求下形成的。最初的 HTTP 视频协议,没有任何特别之处,就是通用的 HTTP 文件渐进式下载,但是在这种情况下,视频无法快进或者跳转到文件尚未被下载到的部分,这就对 HTTP 协议提出了范围请求(Range Request)的要求,目前几乎所有 HTTP 服务器都支持范围请求。所谓范围请求指的是请求文件的部分数据,这可以在 HTTP 请求头中通过 Range 字段设置偏移量来实现。 +所谓流媒体技术就是把连续的影像和声音信息经过压缩处理后放上网站服务器,由视频服务器向用户计算机顺序或实时地传送各个压缩包,让用户一边下载一边观看、收听,而不要等整个压缩文件下载到自己的计算机上才可以观看的网络传输技术。该技术先在使用者端的计算机上创建一个缓冲区,客户端在播放前并不需要下载整个媒体⽂文件,而是在将缓存区中已经收到的媒体数据进⾏行播放。同时,媒体流的剩余部分仍持续不断地从服务器递送到客户端,即所谓的“边下载,边播放”。 -这种方式应用于视频点播还可以,用于直播的话实时性较差,延迟也很高,于是,苹果公司又在 HTTP 协议的基础上推出了 HTTP Live Streaming(简称 HLS)这个流媒体传输协议。 + + +## 流媒体系统的组成 + +通常,组成一个完整的流媒体系统包括以下5个部分: + +- 一种用于创建、捕捉和编辑多媒体数据,形成流媒体格式的编码工具; + +- 流媒体数据; + +- 一个存放和控制流媒体数据的服务器; + +- 要有适合多媒体传输协议甚至是实时传输协议的网络; + +- 供客户端浏览流媒体文件的播放器。 + + + +## 流媒体技术的分类 + +从传输方式上大致可以分为HTTP渐进式下载、实时流媒体传输、HTTP流式传输三大类。 + + + +### HTTP顺序流(渐进)式传输(Progressive Streaming) + +顺序流式传输是顺序下载,在下载文件的同时用户可以观看,但是,用户的观看与服务器上的传输并不是同步进行的,用户是在一段延时后才能看到服务器上传出来的信息,或者说用户看到的总是服务器在若干时间以前传出来的信息。如YouTube、优酷等大型视频网站的点播分发。它的核心区别是媒体文件不分片,直接以完整文件形态进行分发,通过支持Seek,终端播放器可从没下载完成部分中任意选取一个时间点开始播放,如此来满足不用等整个文件下载完快速播放的需求,一般MP4和FLV格式文件支持较好,打开一个视频拖拽到中部,短暂缓冲即可播放,点击暂停后文件仍将被持续下载就是典型的渐进式下载。在这过程中,客户端需要在硬盘上缓存所有前⾯面已经下载的媒体数据,对本地存储空间的需求较⼤大。播放过程中⽤用户只能在前⾯面已经下载媒体数据的时间范围内进⾏行进度条搜索和快进、快退等操作,而无法在整个媒体⽂文件时间范围内执⾏行这些操作。顺序流式传输比较适合高质量的短片段,因为它可以较好地保证节目播放的最终质量。它适合于在网站上发布的供用户点播的音视频节目。 + +- 应用场景 + - 点播型应用 +- 协议 + - HTTP + + + +### 实时流式传输(Realtime Streaming) + +在实时流式传输中,音视频信息可被实时观看到。 +实时流式传输必须匹配连接宽带,意味着以调制解调器速度连接时图像质量较差,而且由于出错丢失的信息被忽略掉,网络拥挤或者出现问题时候,视频质量很差,如欲保证视频质量,顺序流式传输也许更好,实时流式传输需要特定的服务器,如QuickTime Streaming Server/Real Server/Windows Media Server这些服务器允许你对媒体发送进行更多级别的控制,因为而系统设置管理比HTTP服务器更复杂,实时流传输还需要特殊的网络协议,比如RTSP(Real time sreming protocol)或MMS(Microsoft Media Server)这些协议在有防火墙的时候会出现问题,导致用户不能看到一些地点的实时内容。 + +- 应用场景 + - 直播型应用。直播服务模式下,用户只能观看播放的内容,无法进行控制。 + - 会议型应用。会议型应用类似于直播型应用,但是两者有不同的要求,如双向通信等。这对一般双方都要有包括媒体采集的硬件和软件,还有流传输技术。会议型的应用有时候不需要很高的音/视频质量。 +- 协议 + - RTSP + - RTMP + +### HTTP流式传输 + +细分又可以分为: 伪HTTP流和HTTP流。 + + + +**HLS类“伪”HTTP流**:HLS(Apple)、HDS(Adobe)、MSS(Microsoft) 、DASH(MPEG组织)均属于“伪”HTTP流,之所以说他们“伪”,是因为他们在体验上类似“流”,但本质上依然是HTTP文件下载。以上几个协议的原理都一样,就是将媒体数据(文件或者直播信号)进行切割分块,同时建立一个分块对应的索引表,一并存储在HTTP Web服务器中,客户端连续线性的请求这些分块小文件,以HTTP文件方式下载,顺序的进行解码播放,我们就得到了平滑无缝的“流”的体验。 + +- 协议 + - HLS + - HDS + - MSS + - DASH + + + +**HTTP流**: http-flv这样的使用类似RTMP流式协议的HTTP长连接,需由特定流媒体服务器分发的,是真正的HTTP流媒体传输方式,他在延时、首画等体验上跟RTMP等流式协议拥有完全一致的表现,同时继承了部分HTTP的优势。 + +- 协议 + - HTTP FLV + +- 应用场景 + - 点播型应用 + - 直播型应用 + + + +接下来我们以上面传输方式的分类来简单介绍一下。 + +## 相关协议 + +### HTTP + + + +渐进下载流媒体播放采⽤用标准HTTP协议来在Web服务器和客户端之间递送媒体数据。 + +互联网上最初只能传输一些文本类的数据,自从HTTP协议发明之后,就可以传输超文本的音频视频等等内容,这主要就是靠HTTP协议中的MIME。通过它,浏览器就会根据协议中的Content-Type header去选择相应的应用程序去处理相应的内容。如此,才使得流媒体内容通过HTTP协议传输成为可能。 + + + +基于HTTP渐进式下载的流媒体播放仅能⽀持点播而不能支持直播,媒体流数据到达客户端的速率无法精确控制,客户端仍需维持一个与服务器上媒体文件同样大小的缓冲存储空间,在开始播放之前需要等待一段较长的缓冲时间从而导致实时性较差,播放过程中由于⽹网络带宽的波动或分组丢失可能会导致画面停顿或断续等待。为克服这些问题,需要引入专门的流媒体服务器以及相应的实时流媒体传输和控制协议来进行支持。所以就出现了接下来实时流媒体协议。RTSP/RTP实际上由一组在IETF 中标准化的协议所组成,包括RTSP (实时流媒体会话协议),SDP(会话描述协议),RTP (实时传输协议),以及针对不同编解码标准的RTP净载格式等,共同协作来构成⼀一个流媒体协议栈。基于该协议栈的扩展已被 ISMA (互联⽹网流媒体联盟) 和 3GPP (第三代合作伙伴计划) 等组织采纳成为互联⽹网和 3G 移动互联⽹网的流媒体标准。 + + + +4.分析与⽐比较作为最简单和原始的流媒体解决⽅方案,HTTP 渐进式下载唯⼀一显著的优点在于它仅需要维护⼀一个标准的 Web 服务器,⽽而这样的服务器基础设施在互联⽹网中已经普遍存在,其安装和维护的⼯工作量和复杂性⽐比起专门的流媒体服务器来说要简单和容易得多。然⽽而其缺点和不⾜足却也很多,⾸首先是仅适⽤用于点播⽽而不⽀支持直播,其次是缺乏灵活的会话控制功能和智能的流量调节机制,再次是客户端需要硬盘空间以缓存整个⽂文件⽽而不适合于移动设备等。基于 RTSP/RTP 的流媒体系统专门针对⼤大规模流媒体直播和点播等应⽤用⽽而设计,需要专门的流媒体服务器⽀支持,与 HTTP 渐进下载相⽐比主要具有如下优势:•流媒体播放的实时性。与 HTTP 渐进下载客户端需要先缓冲⼀一定数量媒体数据才能开始播放不同,基于 RTSP/RTP 的流媒体客户端⼏几乎在接收到第⼀一帧媒体数据的同时就可以启动播放。•⽀支持进度条搜索、快进、快退等⾼高级 VCR 控制功能。•平滑、流畅的⾳音视频播放体验。在基于 RTSP 的流媒体会话期间,客户端与服务器之间始终保持会话联系,服务器能够对来⾃自客户端的反馈信息动态做出响应。当因⽹网络拥塞等原因导致可⽤用带宽不⾜足时,服务器可通过适当降低帧率等⽅方式来智能调整发送速率。此外,UDP 传输协议的使⽤用使得客户端在检测到有丢包发⽣生时,可选择让服务器仅选择性地重传部分重要的数据(如关键帧),⽽而忽略其他优先级较低的数据,从⽽而保证在⽹网络不好的情况下客户端也仍能连续、流畅地进⾏行播放。尽管如此,基于 RTSP/RTP 的流媒体系统在实际的应⽤用部署特别是移动互联⽹网应⽤用中仍然遇到了不少问题,主要体现在:•与 Web 服务器相⽐比,流媒体服务器的安装、配置和维护都较为复杂,特别是对于已经建有 CDN(内容分发⽹网络)等基础设施的运营商来说,重新安装配置⽀支持RTSP/RTP 的流媒体服务器⼯工作量很⼤大。 + + + + + +### RTSP + +#### RTP + +实时传输协议(Real-time Transport Protocol):是用于Internet上针对多媒体数据流的一种传输层协议,用于实际承载媒体数据并为具有实时特性的媒体数据交互提供端到端的传输服务,例如净载类型识别、序列号、时间戳和传输监控等。RTP是真正的实时传输协议,客户端仅需要维持一个很⼩小的解码缓冲区⽤于缓存视频解码所需的少数参考帧数据,从⽽⼤大缩短了起始播放时延,通常可控制在1秒之内。应⽤用程序通常选择在UDP之上来运⾏行RTP协议,以便利用UDP的复用和校验和等功能,并提高网络传输的有效吞吐量。当因为网络拥塞而发⽣RTP丢包时,服务器可以根据媒体编码特性智能的进行选择性重传,故意丢弃一些不重要的数据包;客户端也可以不必等待未按时到达的数据⽽继续向前播放,从⽽保证媒体播放的流畅性。 + +#### RTCP + +Real-time Transport Control Protocol或RTP Control Protocol实时传输控制协议,是实时传输协议(RTP)的一个姐妹协议。RTCP为RTP媒体流提供信道外(out-of-band)控制。RTCP本身并不传输数据,但和RTP一起协作将多媒体数据打包和发送。RTCP定期在流多媒体会话参加者之间传输控制数据,它的主要功能是收集相关媒体链接的统计信息,并为RTP所提供的服务质量提供反馈。 + +RTCP收集相关媒体连接的统计信息,例如:传输字节数,传输分组数,丢失分组数,jitter,单向和双向网络延迟等等。网络应用程序可以利用RTCP所提供的信息试图提高服务质量,比如限制信息流量或改用压缩比较小的编解码器。 + +#### RTSP + +Real Time Streaming Protocol(实时流传输协议):由哥伦比亚大学、网景和Real Networks公司提交,是一种基于文本的应用层协议,在语法及一些消息参数等方面,RTSP协议与HTTP协议类似。⽤来建立和控制⼀一个或多个时间同步的连续⾳视频媒体流的会话协议。通过在客户机和服务器之间传递 RTSP 会话命令,可以完成诸如请求播放、开始、暂停、查找、快进和快退等VCR控制操作。RTSP 在体系结构上位于RTP和RTCP之上,它使用TCP或UDP完成数据传输。使用RTSP时,客户机和服务器都可以发出请求,即RTSP可以是双向的。允许同时多个串流需求控制,除了可以降低服务器端的网络用量,更进而支持多方视讯会议(Video Conference)。 + + + +RTSP在安防领域有广泛应用,一般传输的是ts/mp4格式的流。 + +- 优点: + - 延迟低,一般都能够做到500ms + - 带宽好,时效率高 + - 倍速播放,主要是回放的时候提供的功能 + - 控制精准,任意选择播放点 +- 缺点 + - 服务端实现复杂 + - 代理服务器弱:数量少,优化少 + - 无路由器防火墙穿透 + - 管流分离:需要1-3个通道 + +#### SDP + +SDP协议⽤用来描述多媒体会话。SDP协议的主要作⽤用在于公告⼀一个多媒体会话中所有媒体流的相关描述信息,以使得接收者能够感知这些描述信息并根据这些描述参与到这个会话中来。SDP会话描述信息通常是通过 RTSP 命令交互来进⾏行传递的,其中携带的媒体类信息主要包括: + +- 媒体的类型(视频,⾳音频等) + +- 传输协议(RTP/UDP/IP,RTP/TCP/IP 等) + +- 媒体编码格式(H.264 视频,AVS 视频等) + +- 流媒体服务器接收媒体流的IP地址和端⼝号 + + + +一次基本的RTSP操作过程是: + +- 首先,客户端连接到流服务器并发送一个RTSP描述命令(DESCRIBE)。 +- 流服务器通过一个SDP描述来进行反馈,反馈信息包括流数量、媒体类型等信息。 +- 客户端再分析该SDP描述,并为会话中的每一个流发送一个RTSP建立命令(SETUP),RTSP建立命令告诉服务器客户端用于接收媒体数据的端口。 +- 流媒体连接建立完成后,客户端发送一个播放命令(PLAY),服务器就开始在UDP上传送媒体流(RTP包)到客户端。 +- 在播放过程中客户端还可以向服务器发送命令来控制快进、快退和暂停等。 +- 最后,客户端可发送一个终止命令(TERADOWN)来结束流媒体会话 + +#### RTSP协议与HTTP协议区别 + +- RTSP引入了几种新的方法,比如DESCRIBE、PLAY、SETUP 等,并且有不同的协议标识符,RTSP为rtsp 1.0,HTTP为http 1.1; + +- HTTP是无状态的协议,而RTSP为每个会话保持状态; + +- RTSP协议的客户端和服务器端都可以发送Request请求,而在HTTPF协议中,只有客户端能发送Request请求。 + +- 在RTSP协议中,载荷数据一般是通过带外方式来传送的(除了交织的情况),及通过RTP协议在不同的通道中来传送载荷数据。而HTTP协议的载荷数据都是通过带内方式传送的,比如请求的网页数据是在回应的消息体中携带的。 + +- 使用ISO10646(UTF-8) 而不是ISO 8859-1,以配合当前HTML的国际化; + +- RTSP使用URI请求时包含绝对URI。而由于历史原因造成的向后兼容性问题,HTTP/1.1只在请求中包含绝对路径,把主机名放入单独的标题域中; + + + +#### RTSP和RTP的关系 + +RTP不象http和ftp可完整的下载整个影视文件,它是以固定的数据率在网络上发送数据,客户端也是按照这种速度观看影视文件,当影视画面播放过后,就不可以再重复播放,除非重新向服务器端要求数据 + +RTSP与RTP最大的区别在于:RTSP是一种双向实时数据传输协议,是纯粹的传输控制协议,它允许客户端向服务器端发送请求,如回放、快进、倒退等操作。当然,RTSP可基于RTP来传送数据,还可以选择TCP、UDP、组播UDP等通道来发送数据,具有很好的扩展性。 + + + + + +![img](https://img-blog.csdn.net/20130925235918343?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdHR0eWQ=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) -RTP ---- -`Real-time Transport Protocol`(实时传输协议):是一种网络传输协议,运行在`UDP` 协议之上,`RTP`协议详细说明了在互联网上传递音频和视频的标准数据包格式。`RTP`协议常用于流媒体系统(配合`RTSP`协议)。 -RTCP ---- -`Real-time Transport Control Protocol`或`RTP Control Protocol`或简写`RTCP`,实时传输控制协议,是实时传输协议(`RTP`)的一个姐妹协议。`RTCP`为`RTP`媒体流提供信道外(`out-of-band`)控制。`RTCP`本身并不传输数据,但和`RTP`一起协作将多媒体数据打包和发送。`RTCP` 定期在流多媒体会话参加者之间传输控制数据。`RTCP`的主要功能是为`RTP`所提供的服务质量(`Quality of Service`)提供反馈。 -RTSP ---- -`Real Time Streaming Protocol`(实时流传输协议实时流传输协议):是用来控制声音或影像的多媒体串流协议,`RTSP`提供了一个可扩展框架,使实时数据,如音频与视频的受控、点播成为可能。该协议定义了一对多应用程序如何有效地通过`IP`网络传送多媒体数据。`RTSP` 在体系结构上位于`RTP`和`RTCP`之上,它使用`TCP`或`UDP`完成数据传输。使用`RTSP`时,客户机和服务器都可以发出请求,即`RTSP`可以是双向的。 -`RTSP`与`RTP`最大的区别在于:`RTSP`是一种双向实时数据传输协议,它允许客户端向服务器端发送请求,如回放、快进、倒退等操作。当然,`RTSP`可基于`RTP`来传送数据,还可以选择`TCP`、`UDP`、组播`UDP`等通道来发送数据,具有很好的扩展性。它时一种类似与`http`协议的网络应用层协议。 RTMP --- -`Real Time Messaging Protocol`(实时消息传送协议):是`Adobe Systems`公司为`Flash`播放器和服务器之间音频、视频和数据传输开发的开放协议。协议基于`TCP`,是一个协议族(默认端口1935),包括`RTMP`基本协议及`RTMPT/RTMPS/RTMPE`等多种变种。`RTMP` 是一种设计用来进行实时数据通信的网络协议,主要用来在`Flash/AIR`平台和支持`RTMP`协议的流媒体/交互服务器之间进行音视频和数据通信。市面上绝大部分PC秀场使用的都是它,他有低延迟(2s左右)、稳定性高、技术完善、高支持度、编码兼容性高等特点。但是RTMP协议不使用标准的HTTP接口传输数据(TCP、UDP端口),所以在一些特殊的网络环境下可能被防火墙屏蔽掉。 -RTMP的整体流程为: +Real Time Messaging Protocol(实时消息传送协议)基于FLV格式进行开发,是Adobe公司为Flash播放器和服务器之间音频、视频和数据传输开发的一种应用层的协议,用来解决多媒体数据传输流的多路复用(Multiplexing)和分包(packetizing)的问题。在基于传输层协议的链接建立完成后,RTMP协议也要客户端和服务器通过“握手”来建立基于传输层链接之上的RTMP Connection链接。协议基于TCP,是一个协议族(默认端口1935),包括RTMP基本协议及RTMPT/RTMPS/RTMPE等多种变种。市面上绝大部分PC秀场使用的都是它,他有低延迟(2s左右)、稳定性高、技术完善、高支持度、编码兼容性高等特点。但是RTMP协议不使用标准的HTTP接口传输数据(TCP、UDP端口),所以在一些特殊的网络环境下可能被防火墙屏蔽掉。 -视频采集器 -> 支持RTMP的视频编码器 -> 网络传输 -> 流媒体服务器 -> 网络 -> 客户端 +使用RTMP技术的流媒体系统有一个非常明显的特点:使用 Flash Player作为播放器客户端,而Flash Player 现在已经安装在了全世界将近99%的PC上,因此一般情况下收看RTMP流媒体系统的视音频是不需要安装插件的。用户只需要打开网页,就可以直接收看流媒体,十分方便。 +采用RTMP协议时,从采集推流端到流媒体服务器再到播放端是一条数据流,因此在服务器不会有落地文件。这样RTMP相对来说就有这些优点: -FLV ---- -`Flash Video`:是Adobe公司推出的另一种视频格式,是一种在网络上传输的流媒体数据存储容器格式。其格式相对简单轻量,不需要很大的媒体头部信息。整个FLV由Header和Body以及其他Tag组成。因此加载速度极快。它是基于HTTP/80传输,可以避免被防火墙拦截的问题,除此之外,它可以通过 HTTP 302 跳转灵活调度/负载均衡,支持使用 HTTPS 加密传输,也能够兼容支持 Android,iOS 的移动端。但是由于它的传输特性,会让流媒体资源缓存在本地客户端,在保密性方面不够好,因为网络流量较大,它也不适合做拉流协议。 +- 实时性高:一般能做到3秒内。 +- 基于 TCP 长连接,不需要多次建连。 -HDS +缺点就是:很多防火墙会墙掉RTMP,但是不会墙掉HTTP。 + + + +Flash Video:是Adobe公司推出的另一种视频格式,是一种在网络上传输的流媒体数据存储容器格式。其格式相对简单轻量,不需要很大的媒体头部信息。整个FLV由Header和Body以及其他Tag组成。因此加载速度极快。 + + + + +HTTP FLV --- -`HTTP Dynamic Streaming`:是Adobe公司的传统流媒体解决方案RTMP+FLV的组合。 +类似RTMP流式协议的HTTP长连接,需由特定流媒体服务器分发的,是真正的HTTP流媒体传输方式,他在延时、首画等体验上跟RTMP等流式协议拥有完全一致的表现,同时继承了部分HTTP的优势。 + +http+flv ,将音视频数据封装成FLV格式,然后通过HTTP协议传输给客户端。http_flv&rtmp这两个协议实际上传输数据是一样的,数据都是flv文件的tag。 + + + + HTTP协议中有个约定:content-length字段,用于描述HTTP消息实体的传输长度。服务器回复http请求的时候如果有这个字段,客户端就接收这个长度的数据然后就认为数据传输完成了,如果服务器回复http请求中没有这个字段,客户端就一直接收数据,直到服务器跟客户端的socket连接断开。http-flv直播就是利用第二个原理,服务器回复客户端请求的时候不加content-length字段,在回复了http内容之后,紧接着发送flv数据,客户端就一直接收数据了,http-flv这种流,服务器是不可能预先知道内容大小的,这时就可以使用Transfer-Encoding:chunk模式来传输数据了。 + + 相比RTMP,HTTP-FLV会生成一个非常大的http流,只能做拉流,RTMP可以做推流/拉流. + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/vs_no_background1.png) + + + +常用的流媒体协议主要有HTTP渐进式下载和基于RTSP/RTP的实时流媒体协议栈等等,这些流媒体协议⼤大多数可以平移到移动流媒体中继续应⽤用。然⽽而由于移动互联⽹网及其终端设备的⼀一些独有特性,传统流媒体协议在移动互联⽹网中的应⽤用在功能、性能的提供和⽤用户体验等⽅方⾯面都会受到不同程度的约束和限制,于是一些新的流媒体协议应运⽽而⽣生。例如,苹果公司的 HTTP Live Streaming 就是其中具有代表性且得到较为⼴广泛应⽤用的一个。 但是HLS(Apple)、HDS(Adobe)、MSS(Microsoft) 、DASH(MPEG组织)均属于“伪”HTTP流,之所以说他们“伪”,是因为他们在体验上类似“流”,但本质上依然是HTTP文件下载。以上几个协议的原理都一样,就是将媒体数据(文件或者直播信号)进行切割分块,同时建立一个分块对应的索引表,一并存储在HTTP Web服务器中,客户端连续线性的请求这些分块小文件,以HTTP文件方式下载,顺序的进行解码播放,我们就得到了平滑无缝的“流”的体验。 + + + +•RTSP/RTP 协议栈的逻辑实现较为复杂,与 HTTP 相⽐比⽀支持 RTSP/RTP 的客户端软硬件实现难度较⼤大,特别是对于嵌⼊入式移动设备终端来说。•RT S P 协议使⽤用的⽹网络端⼜⼝口号(554)可能被部分⽤用户⽹网络中的防⽕火墙和NAT等封堵,导致⽆无法使⽤用。虽然有些流媒体服务器可通过隧道⽅方式将 RTSP 配置在HTTP 的 80 端⼜⼝口上承载,但实际部署起来并不是特别⽅方便。HTTP Live Streaming 正是为了解决这些问题应运⽽而⽣生的,其主要特点是:放弃专门的流媒体服务器,⽽而返回到使⽤用标准的 Web 服务器来递送媒体数据;将容量巨⼤大的连续媒体数据进⾏行分段,分割为数量众多的⼩小⽂文件进⾏行传递,迎合了 Web 服务器的⽂文件传输特性;采⽤用了⼀一个不断更新的轻量级索引⽂文件来控制分割后⼩小媒体⽂文件的下载和播放,可同时⽀支持直播和点播,以及 VCR 类会话控制操作。HTTP 协议的使⽤用降低了HTTP Live Streaming 系统的部署难度,同时也简化了客户端(特别是嵌⼊入式移动终端)软件的开发复杂度。此外,⽂文件分割和索引⽂文件的引⼊入也使得带宽⾃自适应的流间切换、服务器故障保护和媒体加密等变得更加⽅方便。与 RTSP/RTP 相⽐比,HTTP Live Streaming 的最⼤大缺点在于它并⾮非⼀一个真正的实时流媒体系统,在服务器和客户端都存在⼀一定的起始延迟。 + + HLS --- -`HTTP Live Streaming`:是苹果公司实现的基于`HTTP`的流媒体传输协议,可实现流媒体的直播和点播,主要应用在`IOS`系统,为`IOS`设备提供音视频直播和点播方案。`HLS`点播,基本上就是常见的分段`HTTP`点播,不同在于,它的分段非常小。相对于常见的流媒体直播协议,例如`RTMP`协议、`RTSP`协议、`MMS`协议等,`HLS`直播最大的不同在于,直播客户端获取到的,并不是一个完整的数据流。`HLS`协议在服务器端将 -直播数据流存储为连续的、很短时长的媒体文件(`MPEG-TS`格式),而客户端则不断的下载并播放这些小文件。因为服务器端总是会将最新的直播数据生成新的小文件,这样客户端只要不停的按顺序播放从服务器获取到的文件,就实现了直播。由此可见,基本上可以认为,`HLS`是以点播的技术方式来实现直播。由于数据通过`HTTP`协议传输,所以完全不用考虑防火墙或者代理的问题,而且分段文件的时长很短,客户端可以很快的选择和切换码率,以适应不同带宽条件下的播放。不过`HLS`的这种技术特点,决定了它的延迟一般总是会高于普通的流媒体直播协议。它也解决了RTMP协议存在的一些问题,例如RTMP协议不使用标准的HTTP接口传输数据(TCP、UDP端口),所以在一些特殊的网络环境下可能被防火墙屏蔽掉,而HLS使用的是HTTP协议传输数据(80端口),不会遇到被防火墙屏蔽的情况。 +HTTP Live Streaming:是苹果公司实现的基于HTTP的流媒体传输协议,可实现流媒体的直播和点播,HLS点播,基本上就是常见的分段HTTP点播,不同在于,它的分段非常小。相对于常见的流媒体直播协议,例如RTMP协议、RTSP协议等,HLS直播最大的不同在于,直播客户端获取到的,并不是一个完整的数据流。HLS协议在服务器端将直播数据流存储为连续的、很短时长的媒体文件(MPEG-TS格式),而客户端则不断的下载并播放这些小文件。因为服务器端总是会将最新的直播数据生成新的小文件,这样客户端只要不停的按顺序播放从服务器获取到的文件,就实现了直播。由此可见,基本上可以认为,HLS是以点播的技术方式来实现直播。由于数据通过HTTP协议传输,所以完全不用考虑防火墙或者代理的问题,而且分段文件的时长很短,客户端可以很快的选择和切换码率,以适应不同带宽条件下的播放。不过HLS的这种技术特点,决定了它的延迟一般总是会高于普通的流媒体直播协议。它也解决了RTMP协议存在的一些问题,例如RTMP协议不使用标准的HTTP接口传输数据(TCP、UDP端口),所以在一些特殊的网络环境下可能被防火墙屏蔽掉,而HLS使用的是HTTP协议传输数据(80端口),不会遇到被防火墙屏蔽的情况。 + +HLS相比于http-flv的优点就是不需要任何插件,html5可以直播播放,safari,EDGE等浏览器都可以直接播放HLS的视频流。 + +HLS requires that video is packaged in the M2TS transport stream; a format that was designed for broadcast TV rather than for streaming content over the internet. Conversely, both DASH and SmoothStreaming use the fragmented MP4 (fMP4) container format, which was designed specifically with streaming content over the internet in mind. M2TS has many inherent disadvantages compared to fMP4, both for servers and clients, some of which are summarized by Timothy Siglin’s excellent [white paper](http://download.kennisportal.com/KP/Adobe/UnifyingGlobalVideoStrategies.pdf). + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/hls_suport_use.png) + + 在开始一个流媒体会话时,客户端会下载一个包含元数据的extended M3U(m3u8) playlist文件,用于寻找可用的媒体流(将视频分为一个个视频小分片,然后用m3u8索引表进行管理,由于客户端下载到的视频都是5-10秒的完整数据,所以视频的流畅性很好,但是同样也引入了很大的延迟(一般延迟在10-30s左右))。 上面提到了m3u8,那m3u8构成是?直播中m3u8、ts如何实时更新? - 是一个索引地址/播放列表,通过FFmpeg将本地的xxx.mp4进行切片处理,生成m3u8播放列表(索引文件)和N多个 .ts文件,并将其(m3u8、N个ts)放置在本地搭建好的webServer服务器的指定目录下,我就可以得到一个可以实时播放的网址,我们把这个m3u8地址复制到 VLC 上就可以实时观看! -在 HLS 流下,本地视频被分割成一个一个的小切片,一般10秒一个,这些个小切片被 m3u8管理,并且随着终端的FFmpeg 向本地拉流的命令而实时更新,影片进度随着拉流的进度而更新,播放过的片段不在本地保存,自动删除,直到该文件播放完毕或停止,ts 切片会相应的被删除,流停止,影片不会立即停止,影片播放会滞后于拉流一段时间 + 在 HLS 流下,本地视频被分割成一个一个的小切片,一般10秒一个,这些个小切片被 m3u8管理,并且随着终端的FFmpeg 向本地拉流的命令而实时更新,影片进度随着拉流的进度而更新,播放过的片段不在本地保存,自动删除,直到该文件播放完毕或停止,ts 切片会相应的被删除,流停止,影片不会立即停止,影片播放会滞后于拉流一段时间 + + HLS的整体流程为: 视频源文件 -> 服务器(编码器、流分割器) -> 分发器(索引文件、*.ts) -> 传输网络 -> 客户端 + + [HLS的详细介绍](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) HDS --- -`Http Dynamic Streaming`:是一个由Adobe公司模仿HLS协议提出的另一个基于Http的流媒体传输协议。其模式与HLS类似,也是索引文件和媒体切片文件结合的下载方式,但是又要比HLS协议更复杂。 +`Http Dynamic Streaming`:是一个由Adobe公司模仿HLS协议提出的另一个基于Http的流媒体传输协议(传统流媒体解决方案RTMP+FLV的组合)。其模式与HLS类似,也是索引文件和媒体切片文件结合的下载方式,但是又要比HLS协议更复杂。 + +Smooth Streaming +--- + +微软提供的一套解决方案,是IIS的媒体服务扩张,用于支持基于HTTP的自适应串流,它的文件切片为mp4,索引文件为ism/ismc。 + + + +在基于HTTP提供流媒体的方面,到目前为止已经看到了三种方案,苹果的HLS,Adobe HTTP Dynamic Streaming (HDS)和Microsoft Smooth Streaming (MSS),当然,各家用的协议,格式会不一样。于是MPEG呼吁大家做到一起,聊聊出一个统一的标准。 + + DASH --- -DASH(MPEG-DASH)全称为Dynamic Adaptive Streaming over HTTP。是国标标准组MPEG 2014年推出的技术标准,主要目标是形成IP网络承载单一格式的流媒体并提供高效与高质量服务的统一方案,解决多制式传输方案(HTTP Live Streaming, Microsoft Smooth Streaming, HTTP Dynamic Streaming)并存格局下的存储与服务能力浪费、运营高成本与复杂度、系统间互操作弱等问题。 +DASH(MPEG-DASH)全称为Dynamic Adaptive Streaming over HTTP。是国标标准组MPEG( (Moving Picture Experts Group) 2014年推出的技术标准,主要目标是形成IP网络承载单一格式的流媒体并提供高效与高质量服务的统一方案,解决多制式传输方案(HTTP Live Streaming, Microsoft Smooth Streaming, HTTP Dynamic Streaming)并存格局下的存储与服务能力浪费、运营高成本与复杂度、系统间互操作弱等问题。 -DASH是基于HTTP的动态自适应的比特率流技术,使用的传输协议是TCP(有些老的客户端直播会采用UDP协议直播, 例如YY, 齐齐视频等). 和HLS, HDS技术类似, 都是把视频分割成一小段一小段, 通过HTTP协议进行传输,客户端得到之后进行播放;不同的是MPEG-DASH支持MPEG-2 TS、MP4等多种格式, 可以将视频按照多种编码切割, 下载下来的媒体格式既可以是ts文件也可以是mp4文件, 所以当客户端加载视频时, 按照当前的网速和支持的编码加载相应的视频片段进行播放. +DASH是基于HTTP的动态自适应的比特率流技术,使用的传输协议是TCP,也是把视频分割成一小段一小段, 通过HTTP协议进行传输,客户端得到之后进行播放;不同的是MPEG-DASH支持MPEG-2 TS、MP4等多种格式, 可以将视频按照多种编码切割, 下载下来的媒体格式既可以是ts文件也可以是mp4文件, 所以当客户端加载视频时, 按照当前的网速和支持的编码加载相应的视频片段进行播放. 因为是分片的,客户端可以自由选择需要播放的媒体分片,可以实现`Adaptive Bitrate Streaming`技术,不同画质内容无缝切换,提供更好的播放体验。 @@ -85,16 +302,32 @@ DASH的整个流程: 内容生成服务器(编码模块、封装模块) -> 流媒体服务器(MPD、媒体文件、HTTP服务器) <-> DASH客户端(控制引擎、媒体引擎、HTTP接入容器) + + [DASH详细介绍](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) -Smooth Streaming ---- -微软提供的一套解决方案,是IIS的媒体服务扩张,用于支持基于HTTP的自适应串流,它的文件切片为mp4,索引文件为ism/ismc。 ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/dash_compare2.png) -***视频编码标准和音频编码标准是`H.264`和`AAC`,这两种标准分别是当今实际应用中编码效率最高的视频标准和音频标准。*** + + +![hls_hds_mss_dash](https://raw.githubusercontent.com/CharonChui/Pictures/master/hls_hds_mss_dash.png) + +这么多流媒体协议我该选用哪个?需要说明的是,各种流媒体协议都有其生存的理由,比如监控行业、电信行业IPTV就不能没有RTSP,因为这里面所有的监控应用程序太多基于RTSP;比如目前的直播主协议就是RTMP,主要是因为CDN对RTMP支持的最好;再比如Apple终端市场占有率太高,就不能够不去考虑HLS。 + + 每一种流媒体协议都有其应用场景,每一种流媒体协议都有其发展的历史原因,每一种流媒体协议都有其自身的优势和不足。所以,我们需要对各种协议的原理、构成、特性都有所了解,才能在自身应用场景中选择出最佳方案。 + + + +参考: + +- [MPEG-DASH vs. Apple HLS vs. Microsoft Smooth Streaming vs. Adobe HDS](https://bitmovin.com/mpeg-dash-vs-apple-hls-vs-microsoft-smooth-streaming-vs-adobe-hds/) +- [A Survey on Quality of Experience ofHTTP Adaptive Streaming](http://www.comnet.informatik.uni-wuerzburg.de/publikationen/journal-articles/?tx_extbibsonomycsl_publicationlist%5BuserName%5D=uniwue_info3&tx_extbibsonomycsl_publicationlist%5BintraHash%5D=99067f2f003db1689d6ac3880a8e0c22&tx_extbibsonomycsl_publicationlist%5BfileName%5D=jour_126.pdf&tx_extbibsonomycsl_publicationlist%5Baction%5D=download&tx_extbibsonomycsl_publicationlist%5Bcontroller%5D=Document&cHash=92121b0e4d712ba4ccb918e8a1d82c34) +- [渐进式、HLS、DASH、HDS、RTMP协议](http://www.4u4v.net/liu-mei-ti-xie-yi-hu-lian-wang-shi-pin-fen-fa-xie-yi-jie-shao-jian-jin-shi-hlsdashhdsrtmp-xie-yi.html) +- [Real-Time Messaging Protocol](https://en.wikipedia.org/wiki/Real-Time_Messaging_Protocol) +- [HTTP Live Streaming](https://en.wikipedia.org/wiki/HTTP_Live_Streaming) +- [Streaming Protocols: Everything You Need to Know](https://www.wowza.com/blog/streaming-protocols) 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" new file mode 100644 index 00000000..a404777d --- /dev/null +++ "b/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/FLV.md" @@ -0,0 +1,21 @@ +FLV +=== + + + + + + + + + + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/VideoDevelopment/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" similarity index 57% rename from "VideoDevelopment/MP4\346\240\274\345\274\217\350\257\246\350\247\243.md" rename to "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 9c3c5a97..aa7cef68 100644 --- "a/VideoDevelopment/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" @@ -1,7 +1,29 @@ MP4格式详解 === -MP4是一套用于音频、视频信息的压缩编码标准,由国际标准化组织(ISO)和国际电工委员会(IEC)下属的“动态图像专家组”(Moving Picture Experts Group,即MPEG)制定。MPEG-4格式的主要用途在于网上流、光盘、语音发送(视频电话),以及电视广播,是一种常见的多媒体封装格式。 +MP4是一套用于音频、视频信息的压缩编码标准,由国际标准化组织(ISO)和国际电工委员会(IEC)下属的“动态图像专家组”(Moving Picture Experts Group,即MPEG)制定。MP4 实际代表的含义是MPEG-4 Part 14。它只是MPEG标准中的14部分。它主要参考ISO/IEC标准来制定的。MP4主要作用是可以实现快进快放,边下载边播放的效果。MPEG-4格式的主要用途在于网上流、光盘、语音发送(视频电话),以及电视广播,是一种常见的多媒体封装格式。 + +MP4的格式稍微比FLV复杂一些,它是通过嵌的方式来实现整个数据的携带。换句话说,它的每一段内容,都可以变成一个对象,如果需要播放的话,只要得到相应的对象即可。 + + + + + +P4视频文件封装格式是基于QuickTime容器格式定义的,因此参考QuickTime的格式定义对理解MP4文件格式很有帮助。MP4文件格式是一个十分开放的容器,几乎可以用来描述所有的媒体结构,MP4文件中的媒体描述与媒体数据是分开的,并且媒体数据的组织也很自由,不一定要按照时间顺序排列,甚至媒体数据可以直接引用其他文件。同时,MP4也支持流媒体。MP4目前被广泛用于封装h.264视频和AAC音频,是高清视频的代表。 + + + + 现在我们就来看看MP4文件格式到底是什么样的。 + +**1、概述** + + MP4文件中的所有数据都装在box(QuickTime中为atom)中,也就是说MP4文件由若干个box组成,每个box有类型和长度,可以将box理解为一个数据对象块。box中可以包含另一个box,这种box称为container box。一个MP4文件首先会有且只有一个“ftyp”类型的box,作为MP4格式的标志并包含关于文件的一些信息;之后会有且只有一个“moov”类型的box(Movie Box),它是一种container box,子box包含了媒体的metadata信息;MP4文件的媒体数据包含在“mdat”类型的box(Midia Data Box)中,该类型的box也是container box,可以有多个,也可以没有(当媒体数据全部引用其他文件时),媒体数据的结构由metadata进行描述。 + + + + + + MP4文件由若干称为Atom(或称为box)的数据对象组成,每个Atom的起首为四个字节的数据长度(Big Endian)和四个字节的类型标识,数据长度和类型标志都可以扩展,Atom的基本结构是: @@ -76,13 +98,48 @@ mvhd定义了整个movie的特性,通常包含媒体无关的信息,例如 #### mdat(media data box) -该box包含于文件层,可以有多个,也可以没有。用来存储媒体数据。 - +该box包含于文件层,可以有多个,也可以没有。用来存储媒体数据。MP4文件的媒体数据存放在这里。mdat中的数据帧依次存放,每个帧的位置、时间、长度都由moov中的信息指定。 mdat Box 基本上占据了视频大小的 95% 以上,得益于 mp4 边下边播的效果,浏览器获取到了部分 mdat box,就可以进行播放。 ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/mp4_stract.jpg?raw=true) +**普通MP4文件播放时,ftyp与moov box需同时加载完成后,并下载部分mdat box的帧数据后,才能开始播放**。 那对于一些长视频,确实存在文件头过大,从而影响第一帧的加载速度问题。 另外,对于不是很规范的文件,例 `mp4视频文件举例`中moov box基本在文件最后的的MP4文件,还有可能存在视频文件基本下载完成后才能播放的问题。 + + + + + + + + + + + +https://blog.csdn.net/qq_19923217/article/details/95049837 + + + +https://www.villainhr.com/page/2017/08/21/%E5%AD%A6%E5%A5%BD%20MP4%EF%BC%8C%E8%AE%A9%E7%9B%B4%E6%92%AD%E6%9B%B4%E7%BB%99%E5%8A%9B#fragmented%20MP4 + + + + + + + + + + + + + + + + + + + --- - 邮箱 :charon.chui@gmail.com diff --git "a/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/TS.md" "b/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/TS.md" new file mode 100644 index 00000000..451f6787 --- /dev/null +++ "b/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/TS.md" @@ -0,0 +1,94 @@ +TS +=== + + + + + +https://blog.csdn.net/qq_19923217/article/details/94200971 + + + + + +TS 全称是 MPEG2-TS,MPEG2-TS 是一种标准容器格式,传输与存储音视频、节目与系统信息协议数据,广泛应用于数字广播系统,我们日常数字机顶盒接收到的就是 TS(Transport Stream,传输流)流。 + +首先需要先分辨 TS 传输流中几个基本概念 + +ES(Elementary Stream):基本流,直接从编码器出来的数据流,可以是编码过的音频、视频或其他连续码流 +PES(Packetized Elementary Streams):PES 流是 ES 流经过 PES 打包器处理后形成的数据流,在这个过程中完成了将 ES 流分组、加入包头信息 (PTS、DTS 等)操作。PES 流的基本单位是 PES 包,PES 包由包头和 payload 组成 +PS 流(Program Stream):节目流,PS 流由 PS 包组成,而一个 PS 包又由若干个 PES 包组成。一个 PS 包由具有同一时间基准的一个或多个 PES 包复合合成。 +TS 流(Transport Stream):传输流,TS 流由固定长度(188 字节)的 TS 包组成,TS 包是对 PES 包的另一种封装方式,同样由具有同一时间基准的一个或多个 PES 包复合合成。PS 包是不固定长度,而 TS 包为固定长度 + + + + + +MPEG-2 标准中,有两种不同的码流可以输出到信号,一种是节目码流(PS Program Stream),一种是传输流(TS Transport Stream)。 + +PS 流包结构长度可变,一旦某一 PS 包的同步信息丢失,接收机就无法确认下一包的同步位置,导致信息丢失,因此 PS 流适用于合理可靠的媒体,如光盘(DVD),PS 流的后缀名一般为 vob 或 evo。而 TS 传输流不同,TS 流的包结构为固定长度(一般为 188 字节),当传输误码破坏了某一 TS 包的同步信息时,接收机可在固定的位置检测它后面包中的同步信息,从而恢复同步,避免信息丢失,因此 TS 可适用于不太可靠的传输,即地面或卫星传播,TS 流的后缀一般为 ts、mpg、mpeg。 + + + +由于 TS 码流具有较强的抵抗传输误码的能力,因此目前在传输媒体中进行传输的 MPEG-2 码流基本上都采用了 TS + + + +以电视数字信号为例: +1) 原始音视频数据经过压缩编码得到基本流 ES 流 + +生成的 ES 基本流比较大,并且只是 I、P、B 这些视频帧或音频取样信息。 +2) 对 ES 基本流 进行打包生成 PES 流 + +通过 PES 打包器,首先对 ES 基本流进行分组打包,在每一个包前加上包头就构成了 PES 流的基本单位 —— PES 包,对视频 PES 来说,一般是一帧一个包,音频 PES 一般一个包不超过 64KB。 + +PES 包头信息中加入了 PTS、DTS 信息,用与音视频的同步。 +3) 同一时间基准的 PES 包经过 TS 复用器生成 TS 传输包 + +PES 包的长度通常都是远大于 TS 包的长度,一个 PES 包必须由整数个 TS 包来传送,没装满的 TS 包由填充字节填充。PES 包进行 TS 复用时,往往一个 PES 包会分存到多个 TS 包中 + +将 PES 包内容分配到一系列固定长度的传输包(TS Packet)中。TS 流中 TS 传输包头加入了 PCR(节目参考时钟)与 PSI(节目专用信息),其中 PCR 用于解码器的系统时钟恢复。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file 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" new file mode 100644 index 00000000..f39c7036 --- /dev/null +++ "b/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/fMP4 vs ts.md" @@ -0,0 +1,98 @@ +# fMP4 vs ts + +MP4,全称MPEG-4 Part 14,是一种使用MPEG-4的多媒体电脑档案格式,副档名为.mp4,以储存数码音讯及数码视讯为主。另外,MP4又可理解为MP4播放器,MP4播放器是一种集音频、视频、图片浏览、电子书、收音机等于一体的多功能播放器。TS是日本高清摄像机拍摄下进行的封装格式,先来简要介绍一下什么是MPEG2-TS吧。MPEG2格式大家都通过对DVD的接触而多多少少了解了一些,DVD节目中的MPEG2格式,确切地说是MPEG2-PS,全称是Program Stream,而TS的全称则是Transport Stream。MPEG2-PS主要应用于存储的具有固定时长的节目,如DVD电影,而MPEG-TS则主要应用于实时传送的节目,比如实时广播的电视节目。这两种格式的主要区别是什么呢?简单地打个比喻说,你将DVD上的VOB文件的前面一截cut掉(或者干脆就是数据损坏),那么就会导致整个文件无法解码了,而电视节目是你任何时候打开电视机都能解码(收看)的,所以,MPEG2-TS格式的特点就是要求从视频流的任一片段开始都是可以独立解码的。 + + + +## fMP4与ts的区别 + +最主要的就是.ts文件不提供关于时长等信息,你无法在ts文件中去实现音视频的seek操作。fmp4不同于ts,它是提供了时长等信息,可以执行seek到指定位置。 + + + +### 媒体数据与元数据的分离 + +在mp4格式中,元数据可以和媒体数据很好地分开存储,后者都在mdat box中,而在ts中,诸多es流和header/metadata信息是复用在一起的。(TODO:介绍ts中的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切片做好,这样就会带来几个问题: +**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) +针对不同语言的音频,都只需要存一个大文件就够了。 +相对地,在streaming TS流时,不得不在manifest(m3u8)文件中把成百上千个ts分片文件全都老老实实地记录下来。 +**3.服务器的cache效率会降低** +实际的streaming应用场景中,往往需要cdn的支持,经常会被client请求的媒体分片就会存在距离client最近的edge server上。对于fmp4 streaming的情况,因为需要的文件更少,cache命中率也就更高,举个例子:可能某一个audio track会和其他各种video track组合,那么就可以将这个audio track放在edge server上,而不用每次都跟origin server去请求。 +相对地,在streaming TS流时,因为每一个音视频组合的都需要以复用文件的形式存储,组合数又非常多,相当于分母大了,edge server就会有很大的几率没有缓存需要的组合而要去向orgin server请求。 + +### 对Trick-play的支持 + +所谓Trick-play,就是快进、快退、直接跳到章节起点、慢动作播放这些“花式”播放功能。支持这些功能往往意味着要快速找到播放流中的关键帧,以快进播放为例,如果利用fmp4格式的特点,可以通过只读取每个媒体分片的moof加上mdat的起始(包含了关键帧图像)部分即可,说白了就是通过只显示关键帧的方法达到“快进”的视觉效果。因为fmp4格式中可以保证每一个分片一定是以IDR帧开始的,这就使得上述的方案实现起来非常方便。 +相对地,在streaming TS流时,没有办法保证关键帧一定在什么位置,所以你可能需要解析一大堆TS packets才能找到关键帧的位置。 + +### 无缝码流切换 + +无缝码流切换实现的关键在于:当第一个码流播放结束时,也就是发生切换的时间,第二个码流一定要以关键帧开始播放。在streaming TS流时,因为不能保证每一个TS chunk一定以关键帧开始,做码流切换时就意味着要同时download两个码流的相应分片,同时解析两个码流,然后找到关键帧对应的位置,才能切换。同时下载、解析两个码流的媒体内容对网络带宽以及设备性能都形成了挑战。而且有意思的是,如果当前网络环境不佳,player想要切换到低码率码流,结果还要在本来就不好的网络环境下同时进行两个码流的下载,可谓是雪上加霜。 +而在fmp4中,除了保证各个分片一定以IDR帧开始外,还能保证不同码流的分片之间在时间线上是对齐的。而且streaming fmp4流时因为不要求音视频复用存储,也就意味着视频和音频的同步点可以不一样,视频可以全都以GOP边界作为同步点,音频可以都以sync frame作为同步点,这都使得无缝码流切换更简单。 +TODO:介绍在分片中间进行切换的情况 + +### 与DRM的集成 + +所谓DRM即数字版权管理,说白了就是对流进行加密,这东西在国内用的不多,但是在国外可是每一个内容提供商必须要有的东西。和编码标准一样,业界也存在很多DRM方案,为了避免每采用一个新的加密方案就要重新编一个码流,MPEG推出了通用加密(CENC)标准(23001-7 - Common Encryption)。使用这一标准的码流,就可以将一个码流应用于各种不同的DRM方案。在DASH spec中,也定义了Content Protection字段来对应这种加密方案。 + +CENC使用的就是fMP4格式,这是利用了fMP4中音视频可以不复用同时还能提供独立于media data存储的metadata的特点。TS流就享受不了这样的好处了。TODO:介绍TS应用DRM的方法。 + + + + + + + +MPEG-TS is designed for live streaming of events over DVB, UDP multicast, but also over HTTP. It divides the stream in elementary streams, which are segmented in small chunks. System information is sent at regular intervals, so the receiver can start playing the stream any time. + +MPEG-TS isn't good for streaming files, because it doesn't provide info about the duration of the movie or song, as well as the points you can seek to. + + + +## 主要说了以下几点: + +- .ts文件不提供关于时长等信息,你无法在ts文件里去实现音视频的seek操作 +- .mp4不同于ts,是提供了时长等信息,可以执行seek到指定位置 +- .ts文件一般用于m3u8中, 或者提供了流媒体基础信息的前提下使用 +- .mp4文件可以在不下载完全媒体文件的前提下进行seek操作;因为其头部记录moov信息(`moov box 中包含编码、分辨率、码率、帧率、时长、音频采样率等等媒体信息`) + + + + + +https://www.itdaan.com/blog/2018/06/09/8e4ec0afb362459fc6abe8112e82a789.html + + + + + + + + + + + + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/fMP4\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/fMP4\346\240\274\345\274\217\350\257\246\350\247\243.md" new file mode 100644 index 00000000..232f8b0e --- /dev/null +++ "b/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/fMP4\346\240\274\345\274\217\350\257\246\350\247\243.md" @@ -0,0 +1,58 @@ +fMP4格式详解 +=== + +MP4文件的基本单元是“box”,这些box既可以包括data,也可以包括metadata。MP4文件标准允许多种方式来组织data box和metadata box。将metadata放在data之前,客户端应用程序可以在播放video/audio之前获得更多的关于video/audio的信息,因此这种方式在大多数的多媒体应用场景都是比较有用的。但是,在流媒体应用场景,不可能预先保存关于整个流数据的metadata信息,因为不可能提前完全知道。而且,预先保存的metadata越少就意味着越少的开销,因此也可以缩短启动时间。 + +MP4 ISO Base Media文件格式标准允许以fragmented方式组织box,这也就意味着MP4文件可以组织成这样的结构,由一系列的短的metadata/data box对组成,而不是一个长的metadata/data对。Fragmented MP4文件结构如图1所示,图中只给出了两个fragments。 + + +fmp4 是基于 MPEG-4 Part 12 的**流媒体格式**。与普通MP4相比: + +- fmp4不需要一个 moov Box 来进行 initialization +- fmp4 的 moov Box 只包含了一些 track 信息 +- fmp4 的 视频/音频 metadata 信息与数据都存在一个个 moof、mdat 中,它是一个流式的封装格式 + + + +![img](https://bitmovin.com/wp-content/uploads/2019/07/image7.png) + +https://bitmovin.com/wp-content/uploads/2019/07/image7.png + + + + + + + +![](https://upload-images.jianshu.io/upload_images/9570401-03569f6101ecfeea.png?imageMogr2/auto-orient/strip|imageView2/2/w/520) + + + +Fragmented MP4 以 Fragment 的方式存储视频信息。每个 Fragment 由一个 moof box 和一个 mdat box 组成。 + +(2)‘mdat’(media data box) + +和普通MP4文件的‘mdat’一样,用于存放媒体数据,不同的是普通MP4文件只有一个‘mdat’box,而Fragmented MP4文件中,每个fragment都会有一个‘mdat’类型的box。 + +(3)‘moof’(movie fragment box) + +该类型的box存放的是fragment-level的metadata信息,用于描述所在的fragment。该类型的box在普通的MP4文件中是不存在的,而在Fragmented MP4文件中,每个fragment都会有一个‘moof’类型的box。 + +一个‘moof’和一个‘mdat’组成Fragmented MP4文件的一个fragment,这个fragment包含一个video track或audio track,并且包含足够的metadata以保证这部分数据可以单独解码。 + +A fragment consists of a Movie Fragment Box (moof), which is very similar to a Movie Box (moov). It contains the information about the media streams contained in one single fragment. E.g. it contains the timestamp information for the 10 seconds of video, which are stored in the fragment. Each fragment has its own Media Data (mdat) box. + + + + + +Fragmented MP4 中的 moov box 只存储文件级别的媒体信息,因此 moov box 的体积比传统的 MP4 中的 moov box 体积要小很多。 + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file 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" new file mode 100644 index 00000000..79860fd7 --- /dev/null +++ "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" @@ -0,0 +1,195 @@ +视频封装格式 +=== + +音视频组成 +一个完整的视频文件,包括音频、视频和基础元信息,我们常见的视频文件如mp4、mov、flv、avi、rmvb等视频文件,就是一个容器的封装,里面包含了音频和视频两部分,并且都是通过一些特定的编码算法,进行编码压缩过后的。 +H264、Xvid等就是视频编码格式,MP3、AAC等就是音频编码格式。例如:将一个Xvid视频编码文件和一个MP3音频编码文件按AVI封装标准封装以后,就得到一个AVI后缀的视频文件。 +因此,视频转换需要设置的本质就是 + +设置需要的视频编码 +设置需要的音频编码 +选择需要的容器封装 + +一个完整的视频转换设置都至少包括了上面3个步骤。 + + + +编码格式 +音频编码格式 +音频编码格式有如下 + +AAC +AMR +PCM +ogg(ogg vorbis音频) +AC3(DVD 专用音频编码) +DTS(DVD 专用音频编码) +APE(monkey’s 音频) +AU(sun 格式) +WMA + +音频编码方案之间音质比较(AAC,MP3,WMA等)结果: AAC+ > MP3PRO > AAC> RealAudio > WMA > MP3 +目前最常见的音频格式有 Mp3、AC-3、ACC,MP3最广泛的支持最多,AC-3是杜比公司的技术,ACC是MPEG-4中的音频标准,ACC是目前比较先进和具有优势的技术。对应入门,知道有这几种最常见的音频格式足以。 +视频编码格式 +视频编码标准有两大系统: MPEG 和ITU-T,国际上制定视频编解码技术的组织有两个,一个是“国际电联(ITU-T)”,它制定的标准有H.261、H.263、H.263+、H.264等,另一个是“国际标准化组织(ISO)”它制定的标准有MPEG-1、MPEG-2、MPEG-4等。 +常见编码格式有: + +Xvid(MPEG4) +H264 (目前最常用编码格式) +H263 +MPEG1,MPEG2 +AC-1 +RM,RMVB +H.265(目前用的不够多) + +目前最常见的视频编码方式的大致性能排序基本是: MPEG-1/-2 < WMV/7/8 < RM/RMVB < Xvid/Divx < AVC/H.264(由低到高,可能不完全准确)。 +在H.265出来之前,H264是压缩率最高的视频压缩格式,其优势有: + +低码率(Low Bit Rate):和MPEG2和MPEG4 ASP等压缩技术相比,在同等图像质量下,采用H.264技术压缩后的数据量只有MPEG2的1/8,MPEG4的1/3。 +高质量的图象 :H.264能提供连续、流畅的高质量图象(DVD质量)。 +容错能力强 :H.264提供了解决在不稳定网络环境下容易发生的丢包等错误的必要工具。 +网络适应性强 :H.264提供了网络抽象层(Network Abstraction Layer),使得H.264的文件能容易地在不同网络上传输(例如互联网,CDMA,GPRS,WCDMA,CDMA2000等)。 + +H.264最大的优势是具有很高的数据压缩比率,在同等图像质量的条件下,H.264的压缩比是MPEG-2的2倍以上,是MPEG-4的1.5~2倍。举个例子,原始文件的大小如果为88GB,采用MPEG-2压缩标准压缩后变成3.5GB,压缩比为25∶1,而采用H.264压缩标准压缩后变为879MB,从88GB到879MB,H.264的压缩比达到惊人的102∶1。低码率(Low Bit Rate)对H.264的高的压缩比起到了重要的作用,和MPEG-2和MPEG-4 ASP等压缩技术相比,H.264压缩技术将大大节省用户的下载时间和数据流量收费。尤其值得一提的是,H.264在具有高压缩比的同时还拥有高质量流畅的图像,正因为如此,经过H.264压缩的视频数据,在网络传输过程中所需要的带宽更少,也更加经济。 +目前这些常见的视频编码格式实际上都属于有损压缩,包括H264和H265,也是有损编码,有损编码才能在质量得以保证的前提下得到更高的压缩率和更小体积 +存储封装格式 +目前市面常见的存储封装格式有如下: + +AVI (.avi) +ASF(.asf) +WMV (.wmv) +QuickTime ( .mov) +MPEG (.mpg / .mpeg) +MP4 (.mp4) +m2ts (.m2ts / .mts ) +Matroska (.mkv / .mks / .mka ) +RM ( .rm / .rmvb) +TS/PS + + + +## 常见格式 + + + +[AVI](https://baike.baidu.com/item/AVI/213655):微软在90年代初创立的封装标准,是当时为对抗quicktime格式(mov)而推出的,只能支持固定[CBR](https://baike.baidu.com/item/CBR/1022793)[恒定比特率](https://baike.baidu.com/item/恒定比特率/5926922)编码的声音文件。 + +[FLV](https://baike.baidu.com/item/FLV/6623513):针对于[h.263](https://baike.baidu.com/item/h.263/2539746)家族的格式。 + +MKV:万能封装器,有良好的兼容和跨平台性、纠错性,可带 [外挂字幕](https://baike.baidu.com/item/外挂字幕)。 + +MOV:MOV是Quicktime封装。 + +MP4:主要应用于mpeg4的封装 。 + +RM/[RMVB](https://baike.baidu.com/item/RMVB/229903):Real Video,由[RealNetworks](https://baike.baidu.com/item/RealNetworks/1987003)开发的应用于[rmvb](https://baike.baidu.com/item/rmvb/229903)和rm 。 + +TS/PS:PS封装只能在HDDVD原版。 + +[WMV](https://baike.baidu.com/item/WMV/1195900):微软推出的,作为市场竞争。 + + + +Q:[b]为什么把flv叫做流式文件格式? 和mp4,avi不是一样都是音视频的容器吗? 有什么区别?[/b] +一下是我收集的几种解释,每个人有不同的理解,把这些都看一遍,你会理解的更加清晰 + +[quote]通常说的流式文件是可以边传边解的,开始不需要整个文件。特点是有文件头信息(这个不是必需的)和中间打包了,可以直接解析分包,而且文件可以任意大小,而不需要通过索引分包。FLV,MPEG,RMVB等都可以直接依次分包解析,而MP4,AVI一定要依赖索引表才行,而且开始就要固定位置好,如果索引表在尾部,还没办法解析。[/quote] + + +[quote]流媒体文件是指多媒体文件边下载可以边观看的文件。而传统的视频文件需下载完成才能观看,而流媒体主要是下载一部分文件到缓存区,然后再从缓存区里面拿数据~而能作为这种流媒体文件的只有经过特殊编码的格式才适合,而flv、rmvb、mov、asf等格式文件才属于流媒体格式文件~[/quote] + +[quote]对于HTTP协议,流式文件可以使用HTTP分段下载,由于在前面的先播放,所以可以一边下载一边播放,但是对于容器格式的文件,由于客户端不知道如何对文件解析(必须拿到整个文件才能解析),所以不能边下载边播放。 +要实现对容器格式的文件的在线播放,必须要服务器支持流式播放接口,例如RTSP协议[/quote] + + + + + +``` +1、mkv:mkv不等同于音频或视频编码格式,它只是为这些进行过音视频编码的数据提供了一个封装的格式,简单的说就是指定音视频数据在文件中如何排列放置。 +MKV最大的特点就是能容纳多种不同类型编码的视频、音频及字幕流,俗称万能媒体容器。 +MKV加入AVI所没有的EDC错误检测代码,这意味着即使是没有下载完毕的MKV文件也可以顺利回放,这些对AVI来说完全是不可想象的。虽然MKV加入了错误检测代码,但由于采用了新的更高效的组织结构,用MKV封装后的电影还是比AVI源文件要小了约1%,这就是说即使加上了多个字幕,MKV文件的体积也不可能比AVI文件大。 +MKV支持可变帧率,它可在动态画面中使用较大的帧率,而在静态画面中使用较小的帧率,这样可以有效的减少视频文件的体积,并改善动态画面的质量。它的作用比目前广泛使用的VBR(可变码率)更为明显。 + +2、avi 可容纳多种类型的音频和视频流,他的封装格式比较老了,在功能上不能像mkv那样满足更多的需求 + +3、rmvb 是rm的升级版本,vb代表变比特率,意思是在画面平缓的时候采用低比特率,画面变化剧烈的时候采用高比特率,有效降低文件尺寸,又不影响太多画质。一般来说,一个700MB的 DVDrip 采用平均比特率为450Kbps的压缩率,生成的 RMVB 大小仅为400MB,但是画质并没有太大变化。但是由于编码器的关系,在画质上还是略输于h.264,所以现在压缩高清视频时更偏重于使用mkv封装。 + +4、mp4 视频MP4格式实际上指的是使用MPEG-4编码格式、或使用MPEG-4衍生出来的编码格式进行编码的文件,比如DivX、XviD、H.263、H.264、 MS MPEG-4 3688 、 Microsoft Video1 、Microsoft RLE,此种文件格式功能不如mkv丰富。 + +5、flv FLV文件体积小巧,清晰的FLV视频1分钟在1MB左右,一部电影在100MB左右,是普通视频文件体积的1/3。再加上CPU占有率低、视频质量良好等特点使其在网络上盛行,目前网上的几家著名视频共享网站均采用FLV格式文件提供视频 + +6、wmv WMV是微软推出的一种流媒体格式,它是在“同门”的ASF(AdvancedStreamFormat)格式升级延伸来得。在同等视频质量下,WMV格式的文件可以边下载边播放,因此很适合在网上播放和传输。 +可是由于微软本身的局限性其WMV的应用发展并不顺利。第一, WM9是微软的产品它必定要依赖着Windows,Windows 意味着解码部分也要有PC,起码要有PC机的主板。这就大大增加了机顶盒的造价,从而影响了视频广播点播的普及。第二,WMV技术的视频传输延迟非常大,通常要10几秒钟,正是由于这种局限性,目前WMV也仅限于在计算机上浏览WM9视频文件。 +``` + + + + + +对于相同的音视频内容,使用三种不同的封装格式,则文件体积从大到小依次为 + +TS -> MP4 -> FLV + + FLV和MP4封装格式的文件大小基本相等。 + +例如:对于同一个文件,采用相同的编码设置,封装为不同的格式 + +[root@localhost ffmpeg-2.1.1]# ffmpeg -i test_wei.flv -t 10 -vcodec libx264 -x264opts keyint=24 -acodec libfaac -r 24 -y output10s.ts + + + + + +[root@localhost ffmpeg-2.1.1]# ffmpeg -i test_wei.flv -t 10 -vcodec libx264 -x264opts keyint=24 -acodec libfaac -r 24 -y output10s.flv + + + +[root@localhost ffmpeg-2.1.1]# ffmpeg -i test_wei.flv -t 10 -vcodec libx264 -x264opts keyint=24 -acodec libfaac -r 24 -y output10s.mp4 + + + + + +[root@localhost ffmpeg-2.1.1]# ll + +-rw-r--r-- 1 root root **1354272** Apr 18 11:06 output10s.flv + +-rw-r--r-- 1 root root **1355649** Apr 18 11:07 output10s.mp4 + +-rw-r--r-- 1 root root **1492156** Apr 18 11:04 output10s.ts + + + + + +## 封装格式与编码方式的对应 + + + +AVI:可用[MPEG-2](https://baike.baidu.com/item/MPEG-2/214322), DIVX, XVID, WMV3, WMV4, WMV9, H.264 + +WMV:可用WMV3, WMV4, WMV9 + +RM/[RMVB](https://baike.baidu.com/item/RMVB/229903):可用RV40, RV50, RV60, RM8, RM9, RM10 + +MOV:可用MPEG-2, MPEG4-ASP([XVID](https://baike.baidu.com/item/XVID/567063)), H.264 + +MKV:可用所有[视频编码](https://baike.baidu.com/item/视频编码)方案 + + + + + +参考: + +- [Digital container format](https://en.wikipedia.org/wiki/Digital_container_format) +- [Comparison of video container formats](https://en.wikipedia.org/wiki/Comparison_of_video_container_formats) +- [**Supported** Video Formats](https://www.encoding.com/formats/) + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file From 546203d2782b43bf607c2228cddd48dd98e25174 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 11 Jun 2020 20:24:25 +0800 Subject: [PATCH 022/213] add notes --- .../DASH.md" | 49 ++- .../HLS.md" | 56 ++- .../HTTP FLV.md" | 25 +- .../RTMP.md" | 110 ++++- ...22\344\275\223\345\215\217\350\256\256.md" | 413 ++++++++++++++++++ ...32\344\277\241\345\215\217\350\256\256.md" | 337 -------------- .../FLV.md" | 82 ++++ ...74\345\274\217\350\257\246\350\247\243.md" | 38 +- .../TS.md" | 48 +- .../fMP4 vs ts.md" | 30 +- ...74\345\274\217\350\257\246\350\247\243.md" | 26 +- ...01\350\243\205\346\240\274\345\274\217.md" | 191 +++----- 12 files changed, 808 insertions(+), 597 deletions(-) create mode 100644 "VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256.md" delete mode 100644 "VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/\346\265\201\345\252\222\344\275\223\351\200\232\344\277\241\345\215\217\350\256\256.md" diff --git "a/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/DASH.md" "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/DASH.md" index 21743f4a..e96493fa 100644 --- "a/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/DASH.md" +++ "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/DASH.md" @@ -10,6 +10,10 @@ MPEG-DASH是一种自适应比特率流技术,可根据实时网络状况实 ![dash_compare](https://raw.githubusercontent.com/CharonChui/Pictures/master/dash_compare.png) +HLS在16年支持了fmp4,在17年支持了4K。 + + + ## 思想 ![dash_main_idea](https://raw.githubusercontent.com/CharonChui/Pictures/master/dash_main_idea.png) @@ -26,8 +30,6 @@ DASH的整个流程: ![image-20200406164238038](https://raw.githubusercontent.com/CharonChui/Pictures/master/mpd_hierarchical_data.png) - - ```xml @@ -60,19 +62,19 @@ DASH中的重要概念 ### Period -MPD文件包含一个或者多个片段(Period),它表示时间轴上的一段时间,每个片段都有一个起始时间和结束时间,并且包含了一个或者多个适配集合(Adaptation Set)。每个适配集合提供了一个或者多个媒体组件的信息,并包含了多种不同的码率。每个适配集合又是由多个呈现(Representation)组成,每个呈现就是同一个视频的不同特征的版本,如码率、分辨率等特征。由于每个的视频都要被切成固定长度的切片,因此每个呈现包括多个视频切片(Segment),每个视频切片都有一个URL地址,这样客户端就可以通过这个地址向服务器发送HTTP GET请求获取该片段。同一个Period内,意味着可用的媒体内容及其各个可用码率(Representation)不会发生变更。直播情况下,“可能”需要周期地去服务器更新MPD文件,服务器可能会移除旧的已经过时的Period,或是添加新的Period。新的Period中可能会添加新的可用码率或去掉上一个Period中存在的某些码率(Representation)。 +标注了视频的时长信息,也可以看做是更新mpd文件的最长时长,MPD文件包含一个或者多个片段(Period),它表示时间轴上的一段时间,每个片段都有一个起始时间和结束时间,并且包含了一个或者多个适配集合(Adaptation Set)。每个适配集合提供了一个或者多个媒体组件的信息,并包含了多种不同的码率。每个适配集合又是由多个呈现(Representation)组成,每个呈现就是同一个视频的不同特征的版本,如码率、分辨率等特征。由于每个的视频都要被切成固定长度的切片,因此每个呈现包括多个视频切片(Segment),每个视频切片都有一个URL地址,这样客户端就可以通过这个地址向服务器发送HTTP GET请求获取该片段。同一个Period内,意味着可用的媒体内容及其各个可用码率(Representation)不会发生变更。直播情况下,“可能”需要周期地去服务器更新MPD文件,服务器可能会移除旧的已经过时的Period,或是添加新的Period。新的Period中可能会添加新的可用码率或去掉上一个Period中存在的某些码率(Representation)。 ### Adaptation Set - 一个Period由一个或者多个Adaptationset组成。Adaptationset由一组可供切换的不同码率的码流(Representation)组成,这些码流中可能包含一个(ISO profile)或者多个(TS profile)media content components,因为ISO profile的mp4或者fmp4 segment中通常只含有一个视频或者音频内容,而TS profile中的TS segment同时含有视频和音频内容,当同时含有多个media component content时,每个被复用的media content component将被单独描述 +包含了媒体呈现的形式,(视频/音频/字幕)。 一个Period由一个或者多个Adaptationset组成。Adaptationset由一组可供切换的不同码率的码流(Representation)组成,这些码流中可能包含一个(ISO profile)或者多个(TS profile)media content components,因为ISO profile的mp4或者fmp4 segment中通常只含有一个视频或者音频内容,而TS profile中的TS segment同时含有视频和音频内容,当同时含有多个media component content时,每个被复用的media content component将被单独描述 ### Representation -每个Adaptationset包含了一个或者多个Representations,一个Representation包含一个或者多个media streams,每个media stream对应一个media content component。为了适应不同的网络带宽,dash客户端可能会从一个Representation切换到另外一个Representation,如果不支持某个Representation的编码格式,在切换时可以忽略之。 +包含不同的码率、编码方式、帧率信息等。每个Adaptationset包含了一个或者多个Representations,一个Representation包含一个或者多个media streams,每个media stream对应一个media content component。为了适应不同的网络带宽,实际播放的时候,视频会在一个AdaptationSet中的不同Representaiton 之间切换码率,可能会从一个Representation切换到另外一个Representation,会依次请求该Representaiton下不同Segment序列。 ### Segments -Segments可以包含任何媒体数据,关于容器,官方提供了两种建议: ISO base media file format(比如MP4文件格式)和MPEG-2 Transport Stream。 +每一个具体的片段。(1,2,4,6,10s …) Segments可以包含任何媒体数据,关于容器,官方提供了两种建议: ISO base media file format(比如MP4文件格式)和MPEG-2 Transport Stream。 每个Representation会划分为多个Segment。Segment分为4类,其中,最重要的是:Initialization Segment(每个Representation都包含1个Init Seg),Media Segment(每个Representation的媒体内容包含若干Media Seg) @@ -83,6 +85,28 @@ Segments可以包含任何媒体数据,关于容器,官方提供了两种建 - Subsegment Segment可能进一步划分为subsegment,每个subsegment由数个Acess Unit组成,Segment index提供了subsegment相对于Segment的字节范围和presentation time range 。客户端可以先下载Segment index。 + + + + `*_init.mp4`: 初始的mp4文件,相当于视频头,在这个头文件中包含了完整的视频元信息(moov),具体的可以使用 `MP4Box -info` 查看。 + + `*.m4s`: 即上面提到的Segments文件,每个m4s仅包含媒体信息 (moof + mdat),而播放器是不能直接播放这个文件的,需要用支持DASH的播放器从init文件开始播放。 + + + +确切的说,当Adaptation set的属性@segmentAlignment为真(true)时,同一个Adaptation set中的多个Representation之中的媒体段是对齐的,因此,当从一个Representation A切换到另一个Representation B时,若Representation A的第N个媒体段已经下载完成,切换时可直接下载Representation B的第N+1个媒体段。 + +DASH对媒体段定义了三种方式: + +BaseURL:单段表示 + +SegmentList:段列表 + +SegmentTemplate:段模板 + +单段表示是最简单的:每个Representation只有一个媒体段。用BaseURL表示。举例如下: + + ### SAP和无缝切换以及SEEK @@ -139,7 +163,17 @@ Segments可以包含任何媒体数据,关于容器,官方提供了两种建 - 插入广告: 通常,可以在HLS和DASH的实时流中插入广告。为此,只需交换单个视频块。 DASH为此提供了一种有效的方法:标准化的界面允许有效地插入广告。 - 快速频道切换: 您可以在各个通道之间切换的速度取决于最大的子段(块)。块越小,通道更改速度越快。正如引言中已经提到的,HLS块通常长约10秒,而DASH块通常长2至4秒。因此,DASH在这方面领先一步。小块还具有降低代码效率的缺点。具有较小块的播放列表必须比具有较大块的播放列表更频繁地更新。这意味着包含较短视频片段的播放列表必须通过HTTP更频繁地更新。 +# **结构与编码** + +MPEG-DASH支持TS和MP4 / ISO BMFF媒体段。HLS只支持MPEG-2 TS。DASH媒体段通常比HLS短,2至4秒比较常见。DASH不需要特定的编解码器。视频可以使用H264编码,也可以用其他编码,VP9和H265也是比较受欢迎的编码。 +一般而言,与HLS相比,DASH可以提供实质上更低的端对端延迟。这对于现场直播的工作流程很重要。此外, MPEG-DASH的基于模板的MPD不需要更新,可以在网络边缘服务器进行缓存,HLS则需要周期性地更新传播多次。 + +DASH支持索引和基于时间的模版,播放器能够基于公开的时钟,如NTPS,进行同步。这对于多相机的情况下,多个播放器之间同步会比较容易。 + +# **DRM** + +DASH和HLS之间的另一个关键区别是它支持DRM。可是,在DASH中不存在一个单一通用的DRM解决方案。例如,Google的Chrome支持Widevine,而Microsoft的Internet Explorer支持PlayReady。然而,通过使用MPEG-CENC(MPEG通用加密)结合加密媒体扩展(EME),视频流内容可以仅被加密一次。HLS支持AES-128加密,以及苹果自己的DRM,Fairplay。 @@ -164,10 +198,9 @@ Segments可以包含任何媒体数据,关于容器,官方提供了两种建 参考: - [HLS,MPEG-DASH - What is ABR?](http://telestreamblog.telestream.net/2017/05/what-is-abr/) - - [B站我们为什么使用DASH](https://www.bilibili.com/read/cv855111) - - [Adaptive HTTP Streaming Technologies: HLS vs. DASH](https://strivecast.io/hls-vs-mpeg-dash/) +- [自适应流媒体传输](https://blog.csdn.net/nonmarking/article/details/86351147) 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 acfeaf00..ed214e94 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" @@ -4,7 +4,7 @@ HLS协议规定: -- 视频的封装格式是ts(16年发布也支持了fMP4) +- 视频的封装格式是ts(WWDC2016年发布也支持了fMP4) - 视频的编码格式为H264、H265、VP9(17年支持H265、VP9),音频编码格式为MP3、AAC或者AC-3 - 除了TS视频文件本身,还定义了用来控制播放的m3u8文件(文本文件) @@ -18,6 +18,18 @@ HLS点播,基本上就是常见的分段HTTP点播,不同在于,它的分 +## 使用 + +- [Google](https://en.wikipedia.org/wiki/Google) added HTTP Live Streaming support in [Android](https://en.wikipedia.org/wiki/Android_(operating_system)) 3.0 (Honeycomb).[[17\]](https://en.wikipedia.org/wiki/HTTP_Live_Streaming#cite_note-17) +- [HP](https://en.wikipedia.org/wiki/Hewlett-Packard) added HTTP Live Streaming support in [webOS](https://en.wikipedia.org/wiki/Webos) 3.0.5.[[18\]](https://en.wikipedia.org/wiki/HTTP_Live_Streaming#cite_note-18) +- Microsoft added support for HTTP Live Streaming in EdgeHTML rendering engine in Windows 10 in 2015.[[19\]](https://en.wikipedia.org/wiki/HTTP_Live_Streaming#cite_note-19) +- Microsoft added support for HTTP Live Streaming in IIS Media Services 4.0.[[20\]](https://en.wikipedia.org/wiki/HTTP_Live_Streaming#cite_note-IISMS4-20) +- [Yospace](https://en.wikipedia.org/wiki/Yospace) added HTTP Live Streaming support in Yospace HLS Player and SDK for flash version 1.0.[*[citation needed](https://en.wikipedia.org/wiki/Wikipedia:Citation_needed)*] +- [Sling Media](https://en.wikipedia.org/wiki/Sling_Media) added HTTP Live Streaming support to its [Slingboxes](https://en.wikipedia.org/wiki/Slingbox) and its SlingPlayer apps.[[21\]](https://en.wikipedia.org/wiki/HTTP_Live_Streaming#cite_note-21) +- In 2014/15, the [BBC](https://en.wikipedia.org/wiki/BBC) introduced HLS-AAC streams for its live internet radio and on-demand audio services, and supports those streams with its [iPlayer Radio](https://en.wikipedia.org/wiki/IPlayer_Radio) clients.[[22\]](https://en.wikipedia.org/wiki/HTTP_Live_Streaming#cite_note-22) + + + ### HLS架构 HLS的架构分为三部分:Server,CDN,Client 。即服务器、分发组件和客户端。 @@ -139,6 +151,36 @@ cctv1hd-1585920074000.ts +#### 直播流 + +```xml +#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-MEDIA-SEQUENCE:396078 +#EXT-X-TARGETDURATION:10 +#EXTINF:10.000, +cctv1hd-1591683975000.ts +#EXTINF:10.000, +cctv1hd-1591683985000.ts +#EXTINF:10.000, +cctv1hd-1591683995000.ts +#EXTINF:10.000, +cctv1hd-1591684005000.ts +#EXTINF:10.000, +cctv1hd-1591684015000.ts +#EXTINF:10.000, +cctv1hd-1591684025000.ts +``` + +直播流是没有#EXT-X-ENDLIST标签的,也就是说播放器在播放完一个.ts文件后会向服务器再次发送请求m3u8文件的请求。 + +live m3u8文件列表需要不断更新,更新规则: + +1. 移除一个文件播放列表中靠前的(认为已播放的)文件 +2. 不断更新`EXT-X-MEDIA-SEQUENCE`标签,以**步长为1**进行递增 + + + ## HLS 的优势 @@ -150,7 +192,7 @@ cctv1hd-1585920074000.ts ## HLS 的劣势 -- 相比 RTMP 这类长连接协议, 延时较高, 难以用到互动直播场景.HLS 理论延时 = 1 个切片的时长 + 0-1个 td (td 是 EXT-X-TARGETDURATION, 可简单理解为播放器取片的间隔时间) + 0-n 个启动切片(苹果官方建议是请求到 3 个片之后才开始播放) + 播放器最开始请求的片的网络延时(网络连接耗时)。为了追求低延时效果, 可以将切片切的更小, 取片间隔做的更小, 播放器未取到 3 个片就启动播放. 但是, 这些优化方式都会增加 HLS 不稳定和出现错误的风险.通常 HLS 直播延时会达到 20-30s,而高延时对于需要实时互动体验的直播来说是不可接受的。 +- 相比 RTMP 这类长连接协议, 延时较高, 难以用到互动直播场景.HLS 理论延时 = 1 个切片的时长 + 0-1个 td (td 是 EXT-X-TARGETDURATION, 可简单理解为播放器取片的间隔时间) + 0-n 个启动切片(苹果官方建议是请求到 3 个片之后才开始播放) + 播放器最开始请求的片的网络延时(网络连接耗时)。为了追求低延时效果, 可以将切片切的更小, 取片间隔做的更小, 播放器未取到 3 个片就启动播放. 但是, 这些优化方式都会增加 HLS 不稳定和出现错误的风险.通常 HLS 直播延时会达到 20-30s,而高延时对于需要实时互动体验的直播来说是不可接受的。Apple在WWDC2019发布了新的解决方案,可以将延迟从8秒降低到1至2秒。 - 对于点播服务来说, 由于 TS 切片通常较小, 海量碎片在文件分发, 一致性缓存, 存储等方面都有较大挑战. - HLS 基于短连接 HTTP,HTTP 是基于 TCP 的,这就意味着 HLS 需要不断地与服务器建立连接,TCP 每次建立连接时的三次握手、慢启动过程、断开连接时的四次挥手都会产生消耗。 @@ -158,6 +200,8 @@ cctv1hd-1585920074000.ts +#### 测试地址: + CCTV1高清:http://ivi.bupt.edu.cn/hls/cctv1hd.m3u8 CCTV3高清:http://ivi.bupt.edu.cn/hls/cctv3hd.m3u8 CCTV5高清:http://ivi.bupt.edu.cn/hls/cctv5hd.m3u8 @@ -166,6 +210,14 @@ CCTV6高清:http://ivi.bupt.edu.cn/hls/cctv6hd.m3u8 +#### 参考: + +- [HTTP Live Streaming Overview](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/StreamingMediaGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008332-CH1-SW1) + + + + + 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 2ac14110..0f90bba7 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" @@ -1,29 +1,21 @@ HTTP 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之前,我们有必要对FLV adobe 官方标准有个认识,因为HTTP-FLV协议中封装格式使用的是FLV。FLV文件格式标准是写F4V/FLV fileformat spec v10.1的附录E里面的FLVFile Format。 +FLV(Flash Video)是Adobe公司设计开发的一种流行的流媒体格式,其格式相对简单轻量,不需要很大的媒体头部信息。整个FLV由Header和Body以及其他Tag组成。因此加载速度极快。它是基于HTTP/80传输,可以避免被防火墙拦截的问题,除此之外,它可以通过 HTTP 302 跳转灵活调度/负载均衡,支持使用 HTTPS 加密传输,也能够兼容支持 Android,iOS 的移动端。但是由于它的传输特性,会让流媒体资源缓存在本地客户端,在保密性方面不够好,因为网络流量较大,它也不适合做拉流协议。此外,FLV可以使用Flash Player进行播放,而Flash Player插件已经安装在绝大部分浏览器上,这使得通过网页播放FLV视频十分容易。FLV封装格式的文件后缀通常为“.flv”。 -FLV(Flash Video)是Adobe公司设计开发的一种流行的流媒体格式,由于其视频文件体积轻巧、封装简单等特点,使其很适合在互联网上进行应用。此外,FLV可以使用Flash Player进行播放,而Flash Player插件已经安装在绝大部分浏览器上,这使得通过网页播放FLV视频十分容易。FLV封装格式的文件后缀通常为“.flv”。 - -在说HTTP-FLV之前,我们有必要对FLV adobe 官方标准有个认识,因为HTTP-FLV协议中封装格式使用的是FLV。 - -FLV文件格式标准是写F4V/FLV fileformat spec v10.1的附录E里面的FLVFile Format。 +先看看HTTP-FLV长成什么样子:http://ip:port/live/livestream.flv,协议头是http,另外”.flv”这个尾巴是它最明显的特征。 +HttpFlv 就是 http+flv ,将音视频数据封装成FLV格式,然后通过 HTTP 协议传输给客户端。 -Flash Video`:是Adobe公司推出的另一种视频格式,是一种在网络上传输的流媒体数据存储容器格式。其格式相对简单轻量,不需要很大的媒体头部信息。整个FLV由Header和Body以及其他Tag组成。因此加载速度极快。它是基于HTTP/80传输,可以避免被防火墙拦截的问题,除此之外,它可以通过 HTTP 302 跳转灵活调度/负载均衡,支持使用 HTTPS 加密传输,也能够兼容支持 Android,iOS 的移动端。但是由于它的传输特性,会让流媒体资源缓存在本地客户端,在保密性方面不够好,因为网络流量较大,它也不适合做拉流协议。 +下的直播平台中大部分的主线路使用的都是HTTP-FLV协议,备线路多为RTMP。小编随便在Safari中打开几个直播平台房间,一抓包就不难发现使用HTTP-FLV协议的身影:熊猫、斗鱼、虎牙、B站。 @@ -37,7 +29,14 @@ HTTP FLV伪流:支持SEEK,可从未下载的部分开始播放。 HTTP-FLV流:拥有和流式协议RTMP一样的特征,长连接,流式数据。 +#### 4 优点: + +- 服务器兼容性好:基于 HTTP 协议。 +- 低延迟:直接传输 FLV 流,而且基于 HTTP 长链接。 + +#### 5 缺点: +- 播放端兼容性不好:需要 Flash 支持,不支持多音视频流,不便于 Seek。 ▣ HTTP-FLV技术实现 diff --git "a/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/RTMP.md" "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/RTMP.md" index e3918a9d..ed3d7a1a 100644 --- "a/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/RTMP.md" +++ "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/RTMP.md" @@ -3,21 +3,123 @@ RTMP +Real Time Messaging Protocol(实时消息传送协议):是Adobe Systems公司为Flash播放器和服务器之间音频、视频和数据传输开发的开放协议。协议基于`TCP`,是一个协议族(默认端口1935),包括RTMP基本协议及RTMPT/RTMPS/RTMPE等多种变种。RTMP 是一种设计用来进行实时数据通信的网络协议,主要用来在Flash/AIR平台和支持RTMP协议的流媒体/交互服务器之间进行音视频和数据通信。市面上绝大部分PC秀场使用的都是它,他有低延迟(2s左右)、稳定性高、技术完善、高支持度、编码兼容性高等特点。但是RTMP协议不使用标准的HTTP接口传输数据(TCP、UDP端口),所以在一些特殊的网络环境下可能被防火墙屏蔽掉。默认端口号:1935 +RTMP的整体流程为: -http://billchan.me/2019/04/27/livestreamprotocol/ +视频采集器 -> 支持RTMP的视频编码器 -> 网络传输 -> 流媒体服务器 -> 网络 -> 客户端 -Real Time Messaging Protocol(实时消息传送协议):是Adobe Systems公司为`Flash`播放器和服务器之间音频、视频和数据传输开发的开放协议。协议基于`TCP`,是一个协议族(默认端口1935),包括`RTMP`基本协议及`RTMPT/RTMPS/RTMPE`等多种变种。`RTMP` 是一种设计用来进行实时数据通信的网络协议,主要用来在`Flash/AIR`平台和支持`RTMP`协议的流媒体/交互服务器之间进行音视频和数据通信。市面上绝大部分PC秀场使用的都是它,他有低延迟(2s左右)、稳定性高、技术完善、高支持度、编码兼容性高等特点。但是RTMP协议不使用标准的HTTP接口传输数据(TCP、UDP端口),所以在一些特殊的网络环境下可能被防火墙屏蔽掉。 +#### 2 握手流程: -RTMP的整体流程为: +以下为简单握手的流程 -视频采集器 -> 支持RTMP的视频编码器 -> 网络传输 -> 流媒体服务器 -> 网络 -> 客户端 +[![Arb6TU.jpg](https://s2.ax1x.com/2019/03/31/Arb6TU.jpg)](https://imgchr.com/i/Arb6TU) + +#### 2.1 包格式: + +![img](https://upload-images.jianshu.io/upload_images/1720840-23c7fd9d1b1f0fd3.png?imageMogr2/auto-orient/) + +#### 2.2 流程说明: + +##### Step 1:C0 + C1 + +`C0 + C1` 一起发送,其中 C0 为 `1` 个字节,固定为 `0x03`,C1 为 `1536` 个字节,所以包总长度为:`1 + 1535 = 1537`。 + +C0 标记着客户端 RTMP 的版本号,目前用到 RTMP 为第三版,所以为 03。 + +> In C0, this field identifies the RTMP version requested by the client. In S0, this field identifies the RTMP version selected by the server. **The version defined by this specification is 3**. 0-2 are deprecated values used by earlier proprietary products; 4-31 are reserved for future implementations; 32-255 are not allowed (to allow distinguishing RTMP from text-based protocols, which always start with a printable character). + +##### Step 2:S0 + S1 + S2 + +`S0 + S1 + S2` 一起发送,其中 S0 为 `1` 个字节,固定为 `0x03`,S1 和 S2 都为 `1536` 个字节,所以包总长度为:`1 + 1536 + 1536 = 3073`。 + +S0 标记着服务端 RTMP 的版本号,目前用到 RTMP 为第三版,所以为 03。 + +##### Step 3:C2 + +C2 为 `1536` 个字节,`RTMP Server` 接收到 `C2` 意味着**握手成功结束**。 + +#### 3 消息格式(Message) + +握手成功后当然就是进行消息通讯,在 RTMP 中的消息都是切分块(Chunk)来发消息的,而 Chunk 发送时必须遵循在一个 Chunk 发送完成之后才能开始发送下一个 Chunk,每个 Chunk 中带有 MessageId 来代表属于哪个 Message,接受端也会按照这个 id 来将 Chunk 组装成 Message。 + +> 为什么 RTMP要将 Message 拆分成不同的 Chunk 呢? +> +> 通过拆分数据量较大的 Message 可以被拆分成较小的 Message,这样就可以避免优先级低的消息持续发送阻塞优先级高的数据,比如在视频的传输过程中,会包括视频帧,音频帧和 RTMP 控制信息,如果持续发送音频数据或者控制数据的话可能就会造成视频帧的阻塞,然后就会造成看视频时最烦人的卡顿现象。同时对于数据量较小的 Message,可以通过对 Chunk Header 的字段来压缩信息,从而减少信息的传输量。 + +Chunk 的默认大小是 128 字节,在传输过程中,通过一个叫做 Set Chunk Size 的控制信息可以设置 Chunk 数据量的最大值,在发送端和接受端会各自维护一个Chunk Size,可以分别设置这个值来改变自己这一方发送的 Chunk 的最大大小。 + +大一点的 Chunk 减少了计算每个 Chunk 的时间从而减少了 CPU 的占用率,但是它会占用更多的时间在发送上,尤其是在低带宽的网络情况下,很可能会阻塞后面更重要信息的传输。小一点的 Chunk 可以减少这种阻塞问题,但小的 Chunk 会引入过多额外的信息(Chunk 中的 Header),少量多次的传输也可能会造成发送的间断导致不能充分利用高带宽的优势,因此并不适合在高比特率的流中传输。 + +在实际发送时应对要发送的数据用不同的 Chunk Size 去尝试,通过抓包分析等手段得出合适的 Chunk 大小,并且在传输过程中可以根据当前的带宽信息和实际信息的大小动态调整 Chunk 的大小,从而尽量提高 CPU 的利用率并减少信息的阻塞机率。 + +#### 3.1 块格式: + +![img](https://s2.ax1x.com/2019/04/27/EKy8Fx.png) + +#### 3.1.1 Basic Header: + +Basic Header 包含两个字段: + +- **chunk stream id**(流通道id):占用字节不固定,一共有 3 种情况( `3 字节 - type`、`2 字节 - type` 或 `1 字节 - type`)。支持用户自定义 `[3,65599]` 之间的 id,其中0,1,2 由协议保留表示特殊信息。 +- **chunk type**(类型):占用 2 位,固定长度。 + +Basic Header 的长度可能是 1,2,或 3 个字节,其中 type 的长度是固定的(占 2 位),Basic Header 的长度取决于 id 的大小,在足够存储这两个字段的前提下最好用尽量少的字节从而减少由于引入 Header 增加的数据量,还有 type 决定了后面 Message Header 的格式。 + +#### 3.1.2 Message Header: + +Message Header 有可能包含以下四个字段: + +- timestamp(时间戳):占用 3 个字节,因此它最多能表示到 `16777215=0xFFFFFF=224-1`,当它的值超过这个最大值时,这三个字节都置为1,这样实际的 timestamp 会转存到 Extended Timestamp 字段中,接受端在判断 timestamp 字段 24 个位都为 1 时就会去 Extended timestamp中 解析实际的时间戳。 +- message length(消息数据的长度):占用3个字节,表示实际发送的消息的数据如音频帧、视频帧等数据的长度,单位是字节。注意这里是 Message 的长度,也就是 Chunk 属于的 Message 的总数据长度,而不是 Chunk 本身 Data 的数据的长度。 +- message type id(消息的类型id):占用 1 个字节,表示实际发送的数据的类型,如 8 代表音频数据、9 代表视频数据。 +- message stream id(消息的流id):占用 4 个字节,表示该 Chunk 所在的流的id + +Message Header 的格式和长度取决于 Basic Header 的 type,共有 4 种不同的格式: + +##### 当 chunk type = 0 时 : + +Message Header 占用 11 个字节,分别有 timestamp、message length、message type id 和 message stream id。 + +##### 当 chunk type = 1 时 : + +Message Header 占用 7 个字节,分别有 timestamp、message length 和 message type id。 + +- 去掉 msg stream id 的 4个字节,表示此 Chunk 和上一次发的 Chunk 所在的流相同,如果在发送端只和对端有一个流链接的时候可以尽量去采取这种格式。 +- timestamp 和 type=0 时不同,存储的是和上一个 Chunk 的时间差,当它的值超过 3 个字节所能表示的最大值时,3 个字节都置为 1,实际的时间戳差值就会转存到 Extended Timestamp 字段中,接受端在判断timestamp 字段 24 个位都为 1 时就会去 Extended timestamp 中解析时机的与上次时间戳的差值。 + +##### 当 chunk type = 2 时 : + +Message Header 占用 3 个字节,只有 timestamp,和上一个 Chunk 的时间差。 + +相对于type=1 格式又去掉了表示消息长度的 3 个字节和表示消息类型 1 个字节,表示此 Chunk 和上一次发送的 Chunk 所在的流、消息的长度和消息的类型都相同。 + +##### 当 chunk type = 3 时 : + +Message Header 占用 0 个字节 + +它表示这个 Chunk 的 Message Header 和上一个是完全相同的。 + +#### 3.1.3 Extended Timestamp(扩展时间戳): + +上面我们提到在 Chunk 中会有时间戳 timestamp 和时间戳差 timestamp delta,并且它们不会同时存在,只有这两者之一大于 3 个字节能表示的最大数值 `0xFFFFFF=16777215` 时,才会用这个字段来表示真正的时间戳,否则这个字段为 0。扩展时间戳占 4 个字节,能表示的最大数值就是 `0xFFFFFFFF=4294967295`。 + +当扩展时间戳启用时,timestamp 字段或者 timestamp delta 要全置为1,表示应该去扩展时间戳字段来提取真正的时间戳或者时间戳差。注意扩展时间戳存储的是完整值,而不是减去时间戳或者时间戳差的值。(注:这里大概是在讲 extended timestamp 存放的扩展时间戳和扩展时间戳差的区别吧) + +#### 3.1.4 Chunk Data: + +用户层面上真正想要发送的与协议无关的数据,长度在 (0, chunkSize] 之间。 +#### 4 优点: +- 延迟低:1. 从采集推流端到流媒体服务器再到播放端是一条数据流,因此在服务器不会有落地文件。2. 基于 TCP 长连接,不需要多次建连 +#### 5 缺点: +- 服务器兼容性差:使用非常规 80 端口,需要额外支持。 +- 播放端兼容性不好:需要 Flash 支持。 diff --git "a/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256.md" "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256.md" new file mode 100644 index 00000000..fd8ed21d --- /dev/null +++ "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256.md" @@ -0,0 +1,413 @@ +流媒体协议 +=== + + + +## 流媒体 + + + +流媒体(Streaming Media)又叫流式媒体。是指把连续的影像和声音信息经过压缩处理后放上网站服务器,由视频服务器向用户计算机顺序或实时地传送各个压缩包,让用户一边下载一边观看、收听,而不要等整个压缩文件下载到自己的计算机上才可以观看的网络传输技术。该技术先在使用者端的计算机上创建一个缓冲区,客户端在播放前并不需要下载整个媒体⽂文件,而是在将缓存区中已经收到的媒体数据进⾏行播放。同时,媒体流的剩余部分仍持续不断地从服务器递送到客户端,即所谓的“边下载,边播放”。采用流媒体技术使得数据包得以像流水一样发送, 如果没有流媒体技术, 那么我们就要像以前用迅雷下电影一样, 下载整个影片才能观看。这样我们需要在漫长的等待之后(因为受限于带宽,下载通常要花上较长的时间),才可以看到或听到媒体传达的信息。令人欣慰的是,在流媒体技术出现之后,人们便无需再等待媒体完全下载完成了。 + + + +## 流媒体系统的组成 + +通常,组成一个完整的流媒体系统包括以下5个部分: + +- 一种用于创建、捕捉和编辑多媒体数据,形成流媒体格式的编码工具; + +- 流媒体数据; + +- 一个存放和控制流媒体数据的服务器; + +- 要有适合多媒体传输协议甚至是实时传输协议的网络; + +- 供客户端浏览流媒体文件的播放器。 + + + +## 流媒体技术的分类 + +从传输方式上大致可以分为HTTP渐进式下载、实时流媒体传输、HTTP流式传输三大类。 + + + +### HTTP顺序流(渐进)式传输(Progressive Streaming) + +顺序流式传输是顺序下载,在下载文件的同时用户可以观看,但是,用户的观看与服务器上的传输并不是同步进行的,用户是在一段延时后才能看到服务器上传出来的信息,或者说用户看到的总是服务器在若干时间以前传出来的信息。如YouTube、优酷等大型视频网站的点播分发。它的核心区别是媒体文件不分片,直接以完整文件形态进行分发,通过支持Seek,终端播放器可从没下载完成部分中任意选取一个时间点开始播放,如此来满足不用等整个文件下载完快速播放的需求,一般MP4和FLV格式文件支持较好,打开一个视频拖拽到中部,短暂缓冲即可播放,点击暂停后文件仍将被持续下载就是典型的渐进式下载。在这过程中,客户端需要在硬盘上缓存所有前⾯已经下载的媒体数据,对本地存储空间的需求较⼤大。播放过程中⽤用户只能在前⾯面已经下载媒体数据的时间范围内进行进度条搜索和快进、快退等操作,而无法在整个媒体文件时间范围内执⾏行这些操作。顺序流式传输比较适合高质量的短片段,因为它可以较好地保证节目播放的最终质量。它适合于在网站上发布的供用户点播的音视频节目。 + +- 应用场景 + - 点播型应用 +- 协议 + - 基于HTTP协议,HTTP协议并不是流媒体协议 + + + +### 实时流式传输(Realtime Streaming) + +在实时流式传输中,音视频信息可被实时观看到。 +实时流式传输必须匹配连接宽带,意味着以调制解调器速度连接时图像质量较差,而且由于出错丢失的信息被忽略掉,网络拥挤或者出现问题时候,视频质量很差,如欲保证视频质量,顺序流式传输也许更好,实时流式传输需要特定的服务器,如QuickTime Streaming Server/Real Server/Windows Media Server这些服务器允许你对媒体发送进行更多级别的控制,因为而系统设置管理比HTTP服务器更复杂,实时流传输还需要特殊的网络协议,比如RTSP(Real time sreming protocol)或MMS(Microsoft Media Server)这些协议在有防火墙的时候会出现问题,导致用户不能看到一些地点的实时内容。 + +- 应用场景 + - 直播型应用。直播服务模式下,用户只能观看播放的内容,无法进行控制。 + - 会议型应用。会议型应用类似于直播型应用,但是两者有不同的要求,如双向通信等。这对一般双方都要有包括媒体采集的硬件和软件,还有流传输技术。会议型的应用有时候不需要很高的音/视频质量。 +- 协议 + - RTSP + - RTMP + +### HTTP流式传输 + +细分又可以分为: 伪HTTP流和HTTP流。 + +#### HTTP流 + +http-flv这样的使用类似RTMP流式协议的HTTP长连接,需由特定流媒体服务器分发的,是真正的HTTP流媒体传输方式,他在延时、首画等体验上跟RTMP等流式协议拥有完全一致的表现,同时继承了部分HTTP的优势。 + +- 协议 + - HTTP FLV + +- 应用场景 + - 点播型应用 + - 直播型应用 + + + +#### HLS类“伪”HTTP流 + +HLS(Apple)、HDS(Adobe)、MSS(Microsoft) 、DASH(MPEG组织)均属于“伪”HTTP流,之所以说他们“伪”,是因为他们在体验上类似“流”,但本质上依然是HTTP文件下载。以上几个协议的原理都一样,就是将媒体数据(文件或者直播信号)进行切割分块,同时建立一个分块对应的索引表,一并存储在HTTP Web服务器中,客户端连续线性的请求这些分块小文件,以HTTP文件方式下载,顺序的进行解码播放,我们就得到了平滑无缝的“流”的体验。 + +- 协议 + - HLS + - HDS + - MSS + - DASH + + + +接下来我们以上面传输方式的分类来简单介绍一下。 + + + +## 相关协议 + +### HTTP + +渐进下载流媒体播放采⽤用标准HTTP协议来在Web服务器和客户端之间递送媒体数据。 + +互联网上最初只能传输一些文本类的数据,自从HTTP协议发明之后,就可以传输超文本的音频视频等等内容,这主要就是靠HTTP协议中的MIME。通过它,浏览器就会根据协议中的Content-Type header去选择相应的应用程序去处理相应的内容。如此,才使得流媒体内容通过HTTP协议传输成为可能。 + + + +基于HTTP渐进式下载的流媒体播放仅能⽀持点播而不能支持直播,媒体流数据到达客户端的速率无法精确控制,客户端仍需维持一个与服务器上媒体文件同样大小的缓冲存储空间,在开始播放之前需要等待一段较长的缓冲时间从而导致实时性较差,播放过程中由于⽹网络带宽的波动或分组丢失可能会导致画面停顿或断续等待。为克服这些问题,需要引入专门的流媒体服务器以及相应的实时流媒体传输和控制协议来进行支持。所以就出现了接下来实时流媒体协议。 + +RTSP/RTP实际上由一组在IETF 中标准化的协议所组成,包括RTSP (实时流媒体会话协议),SDP(会话描述协议),RTP (实时传输协议),以及针对不同编解码标准的RTP净载格式等,共同协作来构成⼀一个流媒体协议栈。基于该协议栈的扩展已被 ISMA (互联⽹网流媒体联盟) 和 3GPP (第三代合作伙伴计划) 等组织采纳成为互联⽹网和 3G 移动互联⽹网的流媒体标准。 + + + +### RTSP + +#### RTP + +实时传输协议(Real-time Transport Protocol):是用于Internet上针对多媒体数据流的一种传输层协议,用于实际承载媒体数据并为具有实时特性的媒体数据交互提供端到端的传输服务,例如净载类型识别、序列号、时间戳和传输监控等。RTP是真正的实时传输协议,客户端仅需要维持一个很⼩小的解码缓冲区⽤于缓存视频解码所需的少数参考帧数据,从⽽⼤大缩短了起始播放时延,通常可控制在1秒之内。应⽤用程序通常选择在UDP之上来运⾏行RTP协议,以便利用UDP的复用和校验和等功能,并提高网络传输的有效吞吐量。当因为网络拥塞而发⽣RTP丢包时,服务器可以根据媒体编码特性智能的进行选择性重传,故意丢弃一些不重要的数据包;客户端也可以不必等待未按时到达的数据⽽继续向前播放,从⽽保证媒体播放的流畅性。RTP在组建IP网络(managed IP networks)中有良好的表现。但是,目前网络应用已经基本转移到CDN上,CDN大多数都不支持RTP流;此外,RTP包很容易被防火墙拦截;另外,RTP流要求服务端与每一个客户端都保持独立的长连接,这对服务端负载造成巨大的压力。 + +#### RTCP + +Real-time Transport Control Protocol或RTP Control Protocol实时传输控制协议,是实时传输协议(RTP)的一个姐妹协议。RTCP为RTP媒体流提供信道外(out-of-band)控制。RTCP本身并不传输数据,但和RTP一起协作将多媒体数据打包和发送。RTCP定期在流多媒体会话参加者之间传输控制数据,它的主要功能是收集相关媒体链接的统计信息,并为RTP所提供的服务质量提供反馈。 + +RTCP收集相关媒体连接的统计信息,例如:传输字节数,传输分组数,丢失分组数,jitter,单向和双向网络延迟等等。网络应用程序可以利用RTCP所提供的信息试图提高服务质量,比如限制信息流量或改用压缩比较小的编解码器。 + +#### RTSP + +Real Time Streaming Protocol(实时流传输协议):由哥伦比亚大学、网景和Real Networks公司提交,是一种基于文本的应用层协议,在语法及一些消息参数等方面,RTSP协议与HTTP协议类似。⽤来建立和控制⼀个或多个时间同步的连续⾳视频媒体流的会话协议。通过在客户机和服务器之间传递RTSP会话命令,可以完成诸如请求播放、开始、暂停、查找、快进和快退等VCR控制操作。RTSP 在体系结构上位于RTP和RTCP之上,它使用TCP或UDP完成数据传输。使用RTSP时,客户机和服务器都可以发出请求,即RTSP可以是双向的。允许同时多个串流需求控制,除了可以降低服务器端的网络用量,更进而支持多方视讯会议(Video Conference)或者安防。 + +由于安防行业非常多程序都已经是写的RTSP协议支持,要改就要改平台,要么就换支持RTSP协议的设备,那么你做为摄像机厂商,你究竟是支持还是不支持RTSP呢?千千万万的开发商和集成商程序都写好了,默认都是依照你设备支持RTSP的标准做的平台,你设备不支持,就会导致没人买。然后还是要支持RTSP。 + + + +基于 RTSP/RTP 的流媒体系统专门针对大规模流媒体直播和点播等应⽤用⽽设计,需要专门的流媒体服务器支持,与 HTTP 渐进下载相比主要具有如下优势: + +- 流媒体播放的实时性。与HTTP渐进下载客户端需要先缓冲一定数量媒体数据才能开始播放不同,基于RTSP/RTP的流媒体客户端几乎在接收到第一帧媒体数据的同时就可以启动播放。 + +- ⽀持进度条搜索、快进、快退等高级VCR控制功能。 + +- 平滑、流畅的⾳视频播放体验。 + + 在基于RTSP的流媒体会话期间,客户端与服务器之间始终保持会话联系,服务器能够对来自客户端的反馈信息动态做出响应。当因⽹络拥塞等原因导致可用带宽不足时,服务器可通过适当降低帧率等⽅式来智能调整发送速率。此外,UDP 传输协议的使用使得客户端在检测到有丢包发生时,可选择让服务器仅选择性地重传部分重要的数据(如关键帧),而忽略其他优先级较低的数据,从而保证在⽹络不好的情况下客户端也仍能连续、流畅地进行播放。 + +尽管如此,基于RTSP/RTP的流媒体系统在实际的应用部署特别是移动互联⽹应用中仍然遇到了不少问题,主要体现在: + +- 与 Web 服务器相比,流媒体服务器的安装、配置和维护都较为复杂,另外一个方面,目前的CDN都是基于RTMP的,顺势而为吧,特别是对于已经建有CDN内容分发⽹络)等基础设施的运营商来说,重新安装配置支持RTSP/RTP的流媒体服务器⼯作量很大。RTSP+RTP在UDP传输,实际上公网环境下大量的UDP包,容易被防火墙block住。 +- RTSP协议使⽤用的⽹网络端⼜⼝口号(554)可能被部分⽤用户⽹网络中的防⽕火墙和NAT等封堵,导致⽆无法使⽤用。虽然有些流媒体服务器可通过隧道⽅式将RTSP配置在HTTP的80端口上承载,但实际部署起来并不是特别⽅便。 + + + +RTSP在安防领域有广泛应用,一般传输的是ts/mp4格式的流。 + +- 优点: + - 延迟低,一般都能够做到500ms + - 带宽好,时效率高 + - 倍速播放,主要是回放的时候提供的功能 + - 控制精准,任意选择播放点 +- 缺点 + - 服务端实现复杂 + - 代理服务器弱:数量少,优化少 + - 无路由器防火墙穿透 + - 管流分离:需要1-3个通道 + +#### SDP + +SDP协议⽤用来描述多媒体会话。SDP协议的主要作⽤用在于公告⼀一个多媒体会话中所有媒体流的相关描述信息,以使得接收者能够感知这些描述信息并根据这些描述参与到这个会话中来。SDP会话描述信息通常是通过 RTSP 命令交互来进⾏行传递的,其中携带的媒体类信息主要包括: + +- 媒体的类型(视频,⾳音频等) + +- 传输协议(RTP/UDP/IP,RTP/TCP/IP 等) + +- 媒体编码格式(H.264 视频,AVS 视频等) + +- 流媒体服务器接收媒体流的IP地址和端⼝号 + + + +一次基本的RTSP操作过程是: + +- 首先,客户端连接到流服务器并发送一个RTSP描述命令(DESCRIBE)。 +- 流服务器通过一个SDP描述来进行反馈,反馈信息包括流数量、媒体类型等信息。 +- 客户端再分析该SDP描述,并为会话中的每一个流发送一个RTSP建立命令(SETUP),RTSP建立命令告诉服务器客户端用于接收媒体数据的端口。 +- 流媒体连接建立完成后,客户端发送一个播放命令(PLAY),服务器就开始在UDP上传送媒体流(RTP包)到客户端。 +- 在播放过程中客户端还可以向服务器发送命令来控制快进、快退和暂停等。 +- 最后,客户端可发送一个终止命令(TERADOWN)来结束流媒体会话 + +#### RTSP协议与HTTP协议区别 + +- RTSP引入了几种新的方法,比如DESCRIBE、PLAY、SETUP 等,并且有不同的协议标识符,RTSP为rtsp 1.0,HTTP为http 1.1; + +- HTTP是无状态的协议,而RTSP为每个会话保持状态; + +- RTSP协议的客户端和服务器端都可以发送Request请求,而在HTTPF协议中,只有客户端能发送Request请求。 + +- 在RTSP协议中,载荷数据一般是通过带外方式来传送的(除了交织的情况),及通过RTP协议在不同的通道中来传送载荷数据。而HTTP协议的载荷数据都是通过带内方式传送的,比如请求的网页数据是在回应的消息体中携带的。 + +- 使用ISO10646(UTF-8) 而不是ISO 8859-1,以配合当前HTML的国际化; + +- RTSP使用URI请求时包含绝对URI。而由于历史原因造成的向后兼容性问题,HTTP/1.1只在请求中包含绝对路径,把主机名放入单独的标题域中; + + + +#### RTSP和RTP的关系 + +RTP不象http和ftp可完整的下载整个影视文件,它是以固定的数据率在网络上发送数据,客户端也是按照这种速度观看影视文件,当影视画面播放过后,就不可以再重复播放,除非重新向服务器端要求数据 + +RTSP与RTP最大的区别在于:RTSP是一种双向实时数据传输协议,是纯粹的传输控制协议,它允许客户端向服务器端发送请求,如回放、快进、倒退等操作。当然,RTSP可基于RTP来传送数据,还可以选择TCP、UDP、组播UDP等通道来发送数据,具有很好的扩展性。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/rtsp_rtp_rtcp.jpeg) + + + + + +RTMP +--- + +Real Time Messaging Protocol(实时消息传送协议)基于FLV格式进行开发,最初由Macromedia开发,后被Adobe收购,是Adobe公司为Flash播放器和服务器之间音频、视频和数据传输开发的一种应用层的协议,用来解决多媒体数据传输流的多路复用(Multiplexing)和分包(packetizing)的问题。在基于传输层协议的链接建立完成后,RTMP协议也要客户端和服务器通过“握手”来建立基于传输层链接之上的RTMP Connection链接。协议基于TCP,是一个协议族(默认端口1935),包括RTMP基本协议及RTMPT/RTMPS/RTMPE等多种变种。市面上绝大部分PC秀场使用的都是它,他有低延迟(2s左右)、稳定性高、技术完善、高支持度、编码兼容性高等特点。但是RTMP协议不使用标准的HTTP接口传输数据(TCP、UDP端口),所以在一些特殊的网络环境下可能被防火墙屏蔽掉。 + +之前的时候使用RTMP技术的流媒体系统有一个非常明显的特点:使用 Flash Player作为播放器客户端,而Flash Player 现在已经安装在了全世界将近99%的PC上,因此一般情况下收看RTMP流媒体系统的视音频是不需要安装插件的。用户只需要打开网页,就可以直接收看流媒体,十分方便。 + +采用RTMP协议时,从采集推流端到流媒体服务器再到播放端是一条数据流,因此在服务器不会有落地文件。这样RTMP相对来说就有这些优点: + +- 实时性高:一般能做到3秒内。 +- 基于 TCP 长连接,不需要多次建连。 + +缺点就是: + +- 很多防火墙会墙掉RTMP,但是不会墙掉HTTP。 + +- RTMP协议是Adobe的私有协议,未完全公开,RTSP协议和HTTP协议是共有协议,并有专门机构做维护。 + +非常多非常多年前,移动互联网还没那么火。还没有H5。Flash视频和应用非常火的时候,RTMP成为了WEB平台直播的唯一方法,于是各大CDN就开始支持RTMP这个协议,经过了非常多年的发展和磨合,非常多cdn已经对rtmp这个协议非常完美的支持了,这个稳定的过程都是多少运维人员熬夜熬出来的,rtmp的势能惯性,会在中国持续未来非常长的时间。cdn不会对稳定盈利的系统轻易做出变化,相同,越来越多的公司来用rtmp。那么就造成cdn更要做rtmp了。这就是一个循环过程,一般的cdn公司不会轻易去打破,除非你是行业巨头。所以现在RTMP用的很多。 + + + + +HTTP FLV +--- +类似RTMP流式协议的HTTP长连接,RTMP封装在HTTP协议之上的,可以更好的穿透防火墙等,需由特定流媒体服务器分发的,是真正的HTTP流媒体传输方式,他在延时、首画等体验上跟RTMP等流式协议拥有完全一致的表现,同时继承了部分HTTP的优势。 + +http+flv ,将音视频数据封装成FLV格式,然后通过HTTP协议传输给客户端。http_flv&rtmp这两个协议实际上传输数据是一样的,数据都是flv文件的tag。相比RTMP,HTTP-FLV会生成一个非常大的http流,只能做拉流,RTMP可以做推流/拉流.所以目前直播常用的方案就是RTMP推流,HTTP-FLV播放。 + + + +HTTP协议中有个约定:content-length字段,用于描述HTTP消息实体的传输长度。服务器回复http请求的时候如果有这个字段,客户端就接收这个长度的数据然后就认为数据传输完成了,如果服务器回复http请求中没有这个字段,客户端就一直接收数据,直到服务器跟客户端的socket连接断开。http-flv直播就是利用第二个原理,服务器回复客户端请求的时候不加content-length字段,在回复了http内容之后,紧接着发送flv数据,客户端就一直接收数据了,http-flv这种流,服务器是不可能预先知道内容大小的,这时就可以使用Transfer-Encoding:chunk模式来传输数据了。 + + + +#### rtmp和http-flv比较: + +- RTMP: 基于TCP长链接,不需要多次建立链接,延时小,另外小数据包支持加密,隐私性好。 + +- HTTP-FLV: HTTP长链接,将收到的数据立即转发,延时小,使用上只需要在大的音视频数据块头部加一些标记头信息,很简单,在延迟表现和大规模并发上比较成熟,手机端 app 使用很合适,实现方式上分为基于文件和基于包,基于包更实时,基于文件可以看回放。 + +- 穿墙:很多防火墙会墙掉RTMP,但是不会墙HTTP,因此HTTP FLV出现奇怪问题的概率很小。 +- 调度:RTMP也有个302,可惜是播放器as中支持的,HTTP FLV流就支持302方便CDN纠正DNS的错误。 +- 容错:SRS的HTTP FLV回源时可以回多个,和RTMP一样,可以支持多级热备。 +- 简单:FLV是最简单的流媒体封装,HTTP是最广泛的协议,这两个组合在一起维护性更高,比RTMP简单多了。 + + +Flash Video:是Adobe公司推出的另一种视频格式,是一种在网络上传输的流媒体数据存储容器格式。其格式相对简单轻量,不需要很大的媒体头部信息。整个FLV由Header和Body以及其他Tag组成。因此加载速度极快。 + +说到这里不得不说一下Adobe。 + +Adobe于2020年停止开发和分发Flash浏览器插件。我们先来简单回顾一下Flash的一生。 + +九十年代的互联网,由于网速的限制,大部分的网站基本上只有纯文本和简单的图片,基本上不能显示太多的图片和视频。当时主流的多媒体制作软件是Macromedia Director,人们可以用它做一些图片视频,它也是现在Adobe Director的前身。并通过Macromedia Shockwave(现为Adobe Shockware)发布到互联网上,人们可以在装有Shockwave的浏览器上浏览这些多媒体信息。 + +由于带宽的限制,人们对于图片的使用还是很谨慎的,这样做可以让Shockwave文件小一些,但这样做出来的东西也很一般。那个时候使用的格式都是GIF和JPEG,而这样的格式浪费了大量的存储空间。 + +这时,FutureWave出品的FutureSplash Animator出现了,它和Macromedia Director很类似,不过它的图片是基于矢量存储的,这些的格式可扩展性都非常强,分辨率也很高,存储空间却很小,这是因为矢量图的生成可以通过cpu做到,而当时网速很慢,cpu处理速度越来越快,这样做真的很聪明。后来,FutureWave打算卖给Adobe,但被拒绝了,最后Macromedia 收购了这家公司。FutureSplash Animator也重新定名为Macromedia Flash 1.0。它由两部分组成:图形及动画编辑器,媒体播放器。 + +随着网速的提升,人们开始使用Flash来播放视频。1999年到2005年之间是Flash发展的黄金时期,无论是Java,RealNetworks,QuickTime,Windows Media Player,所有的媒体播放器在装机量上都远不及它。 + +Macromedia 对Flash 服务的重视和持续投入改进更加促进了它的增长,后期加入的MovieClips也让它从一个媒体创造平台转型成为了一个网络平台。 + +2005年,Adobe收购了Macromedia之后,继续开发Flash,业务开始涵盖影片、音乐、游戏等诸多领域,许多电脑都已经预装了Flash,而且增加了边下边播的功能,这样用户就可以在文件刚下载时播放视频,Flash也逐渐成为了“行业标准”。 + +上世纪90年代,大多数的浏览器还不支持css,而Flash的出现,人们可以在浏览器上播放动画,火柴人,爆笑三国,神啊救救我吧,等无数的的视频,对老网民来说,留下了无数的回忆。超高压占比的格式,矢量图,边下边播,节省带宽的格式,也给人们留下了深刻的印象。那个时候,人们可以不用考虑代码实现就可以通过Flash做出一些炫酷的动画。 + +业界现在普遍认为,Flash的下坡路是从和苹果的决裂开始的。尤其是乔布斯在2010年发布的一篇《[thoughts-on-flash](https://www.apple.com/hotnews/thoughts-on-flash/)》的文章。乔布斯在里面写下了于Flash的一点看法,说明自己为什么不使用Flash,谈到关于Flash的一些问题,比如开放性,安全性,对于设备续航的影响,不利于触摸屏,等等。最重要的原因。让一个第三方软件插足于开发者和平台之间,只会带来不合标准的应用,阻碍平台的改善与发展,我们不能被第三方的决定所左右。苹果从一开始就作为平台级别的绝对掌控者,从一开始就建立了app store这样的封闭系统策略,Flash 作为一个第三方插件,很难进入苹果自己的平台争利。 + +移动端的乏力并不是Flash唯一的问题,在PC方面,Flash也遇到了大麻烦,2015年谷歌旗下的Youtube开始将视频格式全部转为html5,甚至还推出了工具Swiffy,可以将Flash转换成html5. + +不仅如此,Google旗下的Chrome占据了浏览器市场上的大部分份额,然而Chrome也慢慢把支持Flash变成非默认的选项,用户一般不会主动打开Flash,只有在遇到需要Flash的网站时,Chrome才会提醒用户需不需要打开它。 + +而Firefox,Safari这些也占有一定份额的浏览器,也开始站在了Flash的对立面上,Pc端和移动端的双面乏力,加速了Flash的衰亡。 + +真正的致命一击还是来自html5,它是最新一代的web标准,用户不需要安装额外的插件就可以访问视频和玩游戏,因为几乎所有的主流浏览器都支持新的html5标准。之后又由于flv.js的出现,加速了flash时代的结束。 + +Adobe宣布将于2020年停止对Flash的支持,很快GitHub之上出现一则请愿,请求Adobe将Flash开源。芬兰开发者 Juha Lindstedt 在请愿中提到:Flash是互联网历史上重要的一笔,消灭Flash意味着将来的一代就看不到以前的东西了。这样一来,很多游戏、体验和网站就会被遗忘;所以请求Adobe对Flash进行开源或部分开源,这样一来开源社区就能对Flash插件进行支持,或者至少打造可将swf/fla文件转换至HTML5、WebAssembly代码的工具。 + + + + + +常用的流媒体协议主要有HTTP渐进式下载和基于RTSP/RTP的实时流媒体协议栈等等,这些流媒体协议大多数可以平移到移动流媒体中继续应用。然⽽由于移动互联⽹网及其终端设备的一些独有特性,传统流媒体协议在移动互联⽹中的应⽤用在功能、性能的提供和⽤户体验等⽅方⾯面都会受到不同程度的约束和限制,以及Apple和Adobe的关系,于是一些新的流媒体协议应运而生。例如,苹果公司的 HTTP Live Streaming 就是其中具有代表性且得到较为⼴广泛应⽤用的一个。 但是HLS(Apple)、HDS(Adobe)、MSS(Microsoft) 、DASH(MPEG组织)均属于“伪”HTTP流,之所以说他们“伪”,是因为他们在体验上类似“流”,但本质上依然是HTTP文件下载。以上几个协议的原理都一样,就是将媒体数据(文件或者直播信号)进行切割分块,同时建立一个分块对应的索引表,一并存储在HTTP Web服务器中,客户端连续线性的请求这些分块小文件,以HTTP文件方式下载,顺序的进行解码播放,我们就得到了平滑无缝的“流”的体验。其主要特点是:放弃专门的流媒体服务器,而返回到使用标准的 Web服务器来递送媒体数据;将容量巨大的连续媒体数据进⾏分段,分割为数量众多的小⽂件进行传递,迎合了 Web服务器的⽂件传输特性;采⽤用了⼀个不断更新的轻量级索引⽂件来控制分割后⼩媒体文件的下载和播放,可同时⽀持直播和点播,以及VCR类会话控制操作。HTTP协议的使⽤用降低了HTTP Live Streaming 系统的部署难度,同时也简化了客户端(特别是嵌⼊入式移动终端)软件的开发复杂度。此外,⽂件分割和索引文件的引入也使得带宽⾃适应的流间切换、服务器故障保护和媒体加密等变得更加方便。与RTSP/RTP相比,HTTP Live Streaming的最⼤缺点在于它并非一个真正的实时流媒体系统,在服务器和客户端都存在一定的起始延迟。 + + + + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/vs_no_background1.png) + + + + +HLS +--- +HTTP Live Streaming:是苹果公司实现的基于HTTP的流媒体传输协议,可实现流媒体的直播和点播,HLS点播,基本上就是常见的分段HTTP点播,不同在于,它的分段非常小。相对于常见的流媒体直播协议,例如RTMP协议、RTSP协议等,HLS直播最大的不同在于,直播客户端获取到的,并不是一个完整的数据流。HLS协议在服务器端将直播数据流存储为连续的、很短时长的媒体文件(MPEG-TS格式),而客户端则不断的下载并播放这些小文件。因为服务器端总是会将最新的直播数据生成新的小文件,这样客户端只要不停的按顺序播放从服务器获取到的文件,就实现了直播。由此可见,基本上可以认为,HLS是以点播的技术方式来实现直播。由于数据通过HTTP协议传输,所以完全不用考虑防火墙或者代理的问题,而且分段文件的时长很短,客户端可以很快的选择和切换码率,以适应不同带宽条件下的播放。不过HLS的这种技术特点,决定了它的延迟一般总是会高于普通的流媒体直播协议。它也解决了RTMP协议存在的一些问题,例如RTMP协议不使用标准的HTTP接口传输数据(TCP、UDP端口),所以在一些特殊的网络环境下可能被防火墙屏蔽掉,而HLS使用的是HTTP协议传输数据(80端口),不会遇到被防火墙屏蔽的情况。 + +HLS相比于http-flv的优点就是不需要任何插件,html5可以直播播放,safari,EDGE等浏览器都可以直接播放HLS的视频流。 + + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/hls_suport_use.png) + + + +在开始一个流媒体会话时,客户端会下载一个包含元数据的extended M3U(m3u8) playlist文件,用于寻找可用的媒体流(将视频分为一个个视频小分片,然后用m3u8索引表进行管理,由于客户端下载到的视频都是5-10秒的完整数据,所以视频的流畅性很好,但是同样也引入了很大的延迟(一般延迟在10-30s左右))。 + +上面提到了m3u8,那m3u8构成是?直播中m3u8、ts如何实时更新? + +- 是一个索引地址/播放列表,通过FFmpeg将本地的xxx.mp4进行切片处理,生成m3u8播放列表(索引文件)和N多个 .ts文件,并将其(m3u8、N个ts)放置在本地搭建好的webServer服务器的指定目录下,我就可以得到一个可以实时播放的网址,我们把这个m3u8地址复制到 VLC 上就可以实时观看! + 在 HLS 流下,本地视频被分割成一个一个的小切片,一般10秒一个,这些个小切片被 m3u8管理,并且随着终端的FFmpeg 向本地拉流的命令而实时更新,影片进度随着拉流的进度而更新,播放过的片段不在本地保存,自动删除,直到该文件播放完毕或停止,ts 切片会相应的被删除,流停止,影片不会立即停止,影片播放会滞后于拉流一段时间 + + + +HLS的整体流程为: + +视频源文件 -> 服务器(编码器、流分割器) -> 分发器(索引文件、*.ts) -> 传输网络 -> 客户端 + +HLS的最大缺点就是延迟高,但Apple在WWDC2019发布了新的解决方案,可以将延迟从8秒降低到1至2秒。 + +[HLS的详细介绍](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) + +HDS +--- +`Http Dynamic Streaming`:是一个由Adobe公司模仿HLS协议提出的另一个基于Http的流媒体传输协议(传统流媒体解决方案RTMP+FLV的组合)。 + +Flash Player 和 Flash Media Server 的最新版支持传统的 [RTMP](https://zh.wikipedia.org/w/index.php?title=Real_Time_Messaging_Protocol&action=edit&redlink=1) 协议和 [HTTP](https://zh.wikipedia.org/wiki/Hypertext_Transfer_Protocol) 协议。它是RTMP的后继产品,也增加了对自适应流的支持。其模式与HLS类似,也是索引文件和媒体切片文件结合的下载方式,但是又要比HLS协议更复杂。 + +基于HTTP的流的优势是: + +- 不需要防火墙开普通web浏览器所需端口以外的任何端口 +- 允许视频切片在浏览器、网关和CDN的缓存,从而显著降低源服务器的负载。 + +HDS 的文件格式为 FLV/F4V/MP4,索引文件为 f4m,同时支持直播和时移。 + + + + + +Smooth Streaming +--- + +微软提供的一套解决方案,是IIS的媒体服务扩张,用于支持基于HTTP的自适应串流,它的文件切片为mp4,索引文件为ism/ismc。 + + + +在基于HTTP提供流媒体的方面,到目前为止已经看到了三种方案,苹果的HLS,Adobe HTTP Dynamic Streaming (HDS)和Microsoft Smooth Streaming (MSS),当然,各家用的协议,格式会不一样。于是MPEG呼吁大家做到一起,聊聊出一个统一的标准。 + + + +DASH +--- + +DASH(MPEG-DASH)全称为Dynamic Adaptive Streaming over HTTP。是国标标准组MPEG( (Moving Picture Experts Group) 2014年推出的技术标准,主要目标是形成IP网络承载单一格式的流媒体并提供高效与高质量服务的统一方案,解决多制式传输方案(HTTP Live Streaming, Microsoft Smooth Streaming, HTTP Dynamic Streaming)并存格局下的存储与服务能力浪费、运营高成本与复杂度、系统间互操作弱等问题。 + +DASH是基于HTTP的动态自适应的比特率流技术,使用的传输协议是TCP,也是把视频分割成一小段一小段, 通过HTTP协议进行传输,客户端得到之后进行播放;不同的是MPEG-DASH支持MPEG-2 TS、MP4等多种格式, 可以将视频按照多种编码切割, 下载下来的媒体格式既可以是ts文件也可以是mp4文件, 所以当客户端加载视频时, 按照当前的网速和支持的编码加载相应的视频片段进行播放. + +因为是分片的,客户端可以自由选择需要播放的媒体分片,可以实现Adaptive Bitrate Streaming技术,不同画质内容无缝切换,提供更好的播放体验。 + +YouTube采用DASH!其网页端及移动端APP都使用了DASH。 + +DASH的整个流程: + +内容生成服务器(编码模块、封装模块) -> 流媒体服务器(MPD、媒体文件、HTTP服务器) <-> DASH客户端(控制引擎、媒体引擎、HTTP接入容器) + + + +[DASH详细介绍](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) + + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/dash_compare2.png) + + + +![hls_hds_mss_dash](https://raw.githubusercontent.com/CharonChui/Pictures/master/hls_hds_mss_dash.png) + +这么多流媒体协议我该选用哪个?需要说明的是,各种流媒体协议都有其生存的理由,比如监控行业、电信行业IPTV就不能没有RTSP,因为这里面所有的监控应用程序太多基于RTSP;比如目前的直播主协议就是RTMP,主要是因为CDN对RTMP支持的最好;再比如Apple终端市场占有率太高,就不能够不去考虑HLS。 + + 每一种流媒体协议都有其应用场景,每一种流媒体协议都有其发展的历史原因,每一种流媒体协议都有其自身的优势和不足。所以,我们需要对各种协议的原理、构成、特性都有所了解,才能在自身应用场景中选择出最佳方案。 + + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/live_video_player.png) + + + + + +参考: + +- [MPEG-DASH vs. Apple HLS vs. Microsoft Smooth Streaming vs. Adobe HDS](https://bitmovin.com/mpeg-dash-vs-apple-hls-vs-microsoft-smooth-streaming-vs-adobe-hds/) +- [A Survey on Quality of Experience ofHTTP Adaptive Streaming](http://www.comnet.informatik.uni-wuerzburg.de/publikationen/journal-articles/?tx_extbibsonomycsl_publicationlist%5BuserName%5D=uniwue_info3&tx_extbibsonomycsl_publicationlist%5BintraHash%5D=99067f2f003db1689d6ac3880a8e0c22&tx_extbibsonomycsl_publicationlist%5BfileName%5D=jour_126.pdf&tx_extbibsonomycsl_publicationlist%5Baction%5D=download&tx_extbibsonomycsl_publicationlist%5Bcontroller%5D=Document&cHash=92121b0e4d712ba4ccb918e8a1d82c34) +- [渐进式、HLS、DASH、HDS、RTMP协议](http://www.4u4v.net/liu-mei-ti-xie-yi-hu-lian-wang-shi-pin-fen-fa-xie-yi-jie-shao-jian-jin-shi-hlsdashhdsrtmp-xie-yi.html) +- [Real-Time Messaging Protocol](https://en.wikipedia.org/wiki/Real-Time_Messaging_Protocol) +- [HTTP Live Streaming](https://en.wikipedia.org/wiki/HTTP_Live_Streaming) +- [Streaming Protocols: Everything You Need to Know](https://www.wowza.com/blog/streaming-protocols) + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/\346\265\201\345\252\222\344\275\223\351\200\232\344\277\241\345\215\217\350\256\256.md" "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/\346\265\201\345\252\222\344\275\223\351\200\232\344\277\241\345\215\217\350\256\256.md" deleted file mode 100644 index 402a87e7..00000000 --- "a/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/\346\265\201\345\252\222\344\275\223\351\200\232\344\277\241\345\215\217\350\256\256.md" +++ /dev/null @@ -1,337 +0,0 @@ -流媒体通信协议 -=== - - - -## 流媒体 - - - -流媒体(Streaming Media)又叫流式媒体。是指采用流式传输技术在网络上连续实时播放的媒体格式,如音频、视频或多媒体文件,采用流媒体技术使得数据包得以像流水一样发送, 如果没有流媒体技术, 那么我们就要像以前用迅雷下电影一样, 下载整个影片才能观看。这样我们需要在漫长的等待之后(因为受限于带宽,下载通常要花上较长的时间),才可以看到或听到媒体传达的信息。令人欣慰的是,在流媒体技术出现之后,人们便无需再等待媒体完全下载完成了。 - -所谓流媒体技术就是把连续的影像和声音信息经过压缩处理后放上网站服务器,由视频服务器向用户计算机顺序或实时地传送各个压缩包,让用户一边下载一边观看、收听,而不要等整个压缩文件下载到自己的计算机上才可以观看的网络传输技术。该技术先在使用者端的计算机上创建一个缓冲区,客户端在播放前并不需要下载整个媒体⽂文件,而是在将缓存区中已经收到的媒体数据进⾏行播放。同时,媒体流的剩余部分仍持续不断地从服务器递送到客户端,即所谓的“边下载,边播放”。 - - - -## 流媒体系统的组成 - -通常,组成一个完整的流媒体系统包括以下5个部分: - -- 一种用于创建、捕捉和编辑多媒体数据,形成流媒体格式的编码工具; - -- 流媒体数据; - -- 一个存放和控制流媒体数据的服务器; - -- 要有适合多媒体传输协议甚至是实时传输协议的网络; - -- 供客户端浏览流媒体文件的播放器。 - - - -## 流媒体技术的分类 - -从传输方式上大致可以分为HTTP渐进式下载、实时流媒体传输、HTTP流式传输三大类。 - - - -### HTTP顺序流(渐进)式传输(Progressive Streaming) - -顺序流式传输是顺序下载,在下载文件的同时用户可以观看,但是,用户的观看与服务器上的传输并不是同步进行的,用户是在一段延时后才能看到服务器上传出来的信息,或者说用户看到的总是服务器在若干时间以前传出来的信息。如YouTube、优酷等大型视频网站的点播分发。它的核心区别是媒体文件不分片,直接以完整文件形态进行分发,通过支持Seek,终端播放器可从没下载完成部分中任意选取一个时间点开始播放,如此来满足不用等整个文件下载完快速播放的需求,一般MP4和FLV格式文件支持较好,打开一个视频拖拽到中部,短暂缓冲即可播放,点击暂停后文件仍将被持续下载就是典型的渐进式下载。在这过程中,客户端需要在硬盘上缓存所有前⾯面已经下载的媒体数据,对本地存储空间的需求较⼤大。播放过程中⽤用户只能在前⾯面已经下载媒体数据的时间范围内进⾏行进度条搜索和快进、快退等操作,而无法在整个媒体⽂文件时间范围内执⾏行这些操作。顺序流式传输比较适合高质量的短片段,因为它可以较好地保证节目播放的最终质量。它适合于在网站上发布的供用户点播的音视频节目。 - -- 应用场景 - - 点播型应用 -- 协议 - - HTTP - - - -### 实时流式传输(Realtime Streaming) - -在实时流式传输中,音视频信息可被实时观看到。 -实时流式传输必须匹配连接宽带,意味着以调制解调器速度连接时图像质量较差,而且由于出错丢失的信息被忽略掉,网络拥挤或者出现问题时候,视频质量很差,如欲保证视频质量,顺序流式传输也许更好,实时流式传输需要特定的服务器,如QuickTime Streaming Server/Real Server/Windows Media Server这些服务器允许你对媒体发送进行更多级别的控制,因为而系统设置管理比HTTP服务器更复杂,实时流传输还需要特殊的网络协议,比如RTSP(Real time sreming protocol)或MMS(Microsoft Media Server)这些协议在有防火墙的时候会出现问题,导致用户不能看到一些地点的实时内容。 - -- 应用场景 - - 直播型应用。直播服务模式下,用户只能观看播放的内容,无法进行控制。 - - 会议型应用。会议型应用类似于直播型应用,但是两者有不同的要求,如双向通信等。这对一般双方都要有包括媒体采集的硬件和软件,还有流传输技术。会议型的应用有时候不需要很高的音/视频质量。 -- 协议 - - RTSP - - RTMP - -### HTTP流式传输 - -细分又可以分为: 伪HTTP流和HTTP流。 - - - -**HLS类“伪”HTTP流**:HLS(Apple)、HDS(Adobe)、MSS(Microsoft) 、DASH(MPEG组织)均属于“伪”HTTP流,之所以说他们“伪”,是因为他们在体验上类似“流”,但本质上依然是HTTP文件下载。以上几个协议的原理都一样,就是将媒体数据(文件或者直播信号)进行切割分块,同时建立一个分块对应的索引表,一并存储在HTTP Web服务器中,客户端连续线性的请求这些分块小文件,以HTTP文件方式下载,顺序的进行解码播放,我们就得到了平滑无缝的“流”的体验。 - -- 协议 - - HLS - - HDS - - MSS - - DASH - - - -**HTTP流**: http-flv这样的使用类似RTMP流式协议的HTTP长连接,需由特定流媒体服务器分发的,是真正的HTTP流媒体传输方式,他在延时、首画等体验上跟RTMP等流式协议拥有完全一致的表现,同时继承了部分HTTP的优势。 - -- 协议 - - HTTP FLV - -- 应用场景 - - 点播型应用 - - 直播型应用 - - - -接下来我们以上面传输方式的分类来简单介绍一下。 - -## 相关协议 - -### HTTP - - - -渐进下载流媒体播放采⽤用标准HTTP协议来在Web服务器和客户端之间递送媒体数据。 - -互联网上最初只能传输一些文本类的数据,自从HTTP协议发明之后,就可以传输超文本的音频视频等等内容,这主要就是靠HTTP协议中的MIME。通过它,浏览器就会根据协议中的Content-Type header去选择相应的应用程序去处理相应的内容。如此,才使得流媒体内容通过HTTP协议传输成为可能。 - - - -基于HTTP渐进式下载的流媒体播放仅能⽀持点播而不能支持直播,媒体流数据到达客户端的速率无法精确控制,客户端仍需维持一个与服务器上媒体文件同样大小的缓冲存储空间,在开始播放之前需要等待一段较长的缓冲时间从而导致实时性较差,播放过程中由于⽹网络带宽的波动或分组丢失可能会导致画面停顿或断续等待。为克服这些问题,需要引入专门的流媒体服务器以及相应的实时流媒体传输和控制协议来进行支持。所以就出现了接下来实时流媒体协议。RTSP/RTP实际上由一组在IETF 中标准化的协议所组成,包括RTSP (实时流媒体会话协议),SDP(会话描述协议),RTP (实时传输协议),以及针对不同编解码标准的RTP净载格式等,共同协作来构成⼀一个流媒体协议栈。基于该协议栈的扩展已被 ISMA (互联⽹网流媒体联盟) 和 3GPP (第三代合作伙伴计划) 等组织采纳成为互联⽹网和 3G 移动互联⽹网的流媒体标准。 - - - -4.分析与⽐比较作为最简单和原始的流媒体解决⽅方案,HTTP 渐进式下载唯⼀一显著的优点在于它仅需要维护⼀一个标准的 Web 服务器,⽽而这样的服务器基础设施在互联⽹网中已经普遍存在,其安装和维护的⼯工作量和复杂性⽐比起专门的流媒体服务器来说要简单和容易得多。然⽽而其缺点和不⾜足却也很多,⾸首先是仅适⽤用于点播⽽而不⽀支持直播,其次是缺乏灵活的会话控制功能和智能的流量调节机制,再次是客户端需要硬盘空间以缓存整个⽂文件⽽而不适合于移动设备等。基于 RTSP/RTP 的流媒体系统专门针对⼤大规模流媒体直播和点播等应⽤用⽽而设计,需要专门的流媒体服务器⽀支持,与 HTTP 渐进下载相⽐比主要具有如下优势:•流媒体播放的实时性。与 HTTP 渐进下载客户端需要先缓冲⼀一定数量媒体数据才能开始播放不同,基于 RTSP/RTP 的流媒体客户端⼏几乎在接收到第⼀一帧媒体数据的同时就可以启动播放。•⽀支持进度条搜索、快进、快退等⾼高级 VCR 控制功能。•平滑、流畅的⾳音视频播放体验。在基于 RTSP 的流媒体会话期间,客户端与服务器之间始终保持会话联系,服务器能够对来⾃自客户端的反馈信息动态做出响应。当因⽹网络拥塞等原因导致可⽤用带宽不⾜足时,服务器可通过适当降低帧率等⽅方式来智能调整发送速率。此外,UDP 传输协议的使⽤用使得客户端在检测到有丢包发⽣生时,可选择让服务器仅选择性地重传部分重要的数据(如关键帧),⽽而忽略其他优先级较低的数据,从⽽而保证在⽹网络不好的情况下客户端也仍能连续、流畅地进⾏行播放。尽管如此,基于 RTSP/RTP 的流媒体系统在实际的应⽤用部署特别是移动互联⽹网应⽤用中仍然遇到了不少问题,主要体现在:•与 Web 服务器相⽐比,流媒体服务器的安装、配置和维护都较为复杂,特别是对于已经建有 CDN(内容分发⽹网络)等基础设施的运营商来说,重新安装配置⽀支持RTSP/RTP 的流媒体服务器⼯工作量很⼤大。 - - - - - -### RTSP - -#### RTP - -实时传输协议(Real-time Transport Protocol):是用于Internet上针对多媒体数据流的一种传输层协议,用于实际承载媒体数据并为具有实时特性的媒体数据交互提供端到端的传输服务,例如净载类型识别、序列号、时间戳和传输监控等。RTP是真正的实时传输协议,客户端仅需要维持一个很⼩小的解码缓冲区⽤于缓存视频解码所需的少数参考帧数据,从⽽⼤大缩短了起始播放时延,通常可控制在1秒之内。应⽤用程序通常选择在UDP之上来运⾏行RTP协议,以便利用UDP的复用和校验和等功能,并提高网络传输的有效吞吐量。当因为网络拥塞而发⽣RTP丢包时,服务器可以根据媒体编码特性智能的进行选择性重传,故意丢弃一些不重要的数据包;客户端也可以不必等待未按时到达的数据⽽继续向前播放,从⽽保证媒体播放的流畅性。 - -#### RTCP - -Real-time Transport Control Protocol或RTP Control Protocol实时传输控制协议,是实时传输协议(RTP)的一个姐妹协议。RTCP为RTP媒体流提供信道外(out-of-band)控制。RTCP本身并不传输数据,但和RTP一起协作将多媒体数据打包和发送。RTCP定期在流多媒体会话参加者之间传输控制数据,它的主要功能是收集相关媒体链接的统计信息,并为RTP所提供的服务质量提供反馈。 - -RTCP收集相关媒体连接的统计信息,例如:传输字节数,传输分组数,丢失分组数,jitter,单向和双向网络延迟等等。网络应用程序可以利用RTCP所提供的信息试图提高服务质量,比如限制信息流量或改用压缩比较小的编解码器。 - -#### RTSP - -Real Time Streaming Protocol(实时流传输协议):由哥伦比亚大学、网景和Real Networks公司提交,是一种基于文本的应用层协议,在语法及一些消息参数等方面,RTSP协议与HTTP协议类似。⽤来建立和控制⼀一个或多个时间同步的连续⾳视频媒体流的会话协议。通过在客户机和服务器之间传递 RTSP 会话命令,可以完成诸如请求播放、开始、暂停、查找、快进和快退等VCR控制操作。RTSP 在体系结构上位于RTP和RTCP之上,它使用TCP或UDP完成数据传输。使用RTSP时,客户机和服务器都可以发出请求,即RTSP可以是双向的。允许同时多个串流需求控制,除了可以降低服务器端的网络用量,更进而支持多方视讯会议(Video Conference)。 - - - -RTSP在安防领域有广泛应用,一般传输的是ts/mp4格式的流。 - -- 优点: - - 延迟低,一般都能够做到500ms - - 带宽好,时效率高 - - 倍速播放,主要是回放的时候提供的功能 - - 控制精准,任意选择播放点 -- 缺点 - - 服务端实现复杂 - - 代理服务器弱:数量少,优化少 - - 无路由器防火墙穿透 - - 管流分离:需要1-3个通道 - -#### SDP - -SDP协议⽤用来描述多媒体会话。SDP协议的主要作⽤用在于公告⼀一个多媒体会话中所有媒体流的相关描述信息,以使得接收者能够感知这些描述信息并根据这些描述参与到这个会话中来。SDP会话描述信息通常是通过 RTSP 命令交互来进⾏行传递的,其中携带的媒体类信息主要包括: - -- 媒体的类型(视频,⾳音频等) - -- 传输协议(RTP/UDP/IP,RTP/TCP/IP 等) - -- 媒体编码格式(H.264 视频,AVS 视频等) - -- 流媒体服务器接收媒体流的IP地址和端⼝号 - - - -一次基本的RTSP操作过程是: - -- 首先,客户端连接到流服务器并发送一个RTSP描述命令(DESCRIBE)。 -- 流服务器通过一个SDP描述来进行反馈,反馈信息包括流数量、媒体类型等信息。 -- 客户端再分析该SDP描述,并为会话中的每一个流发送一个RTSP建立命令(SETUP),RTSP建立命令告诉服务器客户端用于接收媒体数据的端口。 -- 流媒体连接建立完成后,客户端发送一个播放命令(PLAY),服务器就开始在UDP上传送媒体流(RTP包)到客户端。 -- 在播放过程中客户端还可以向服务器发送命令来控制快进、快退和暂停等。 -- 最后,客户端可发送一个终止命令(TERADOWN)来结束流媒体会话 - -#### RTSP协议与HTTP协议区别 - -- RTSP引入了几种新的方法,比如DESCRIBE、PLAY、SETUP 等,并且有不同的协议标识符,RTSP为rtsp 1.0,HTTP为http 1.1; - -- HTTP是无状态的协议,而RTSP为每个会话保持状态; - -- RTSP协议的客户端和服务器端都可以发送Request请求,而在HTTPF协议中,只有客户端能发送Request请求。 - -- 在RTSP协议中,载荷数据一般是通过带外方式来传送的(除了交织的情况),及通过RTP协议在不同的通道中来传送载荷数据。而HTTP协议的载荷数据都是通过带内方式传送的,比如请求的网页数据是在回应的消息体中携带的。 - -- 使用ISO10646(UTF-8) 而不是ISO 8859-1,以配合当前HTML的国际化; - -- RTSP使用URI请求时包含绝对URI。而由于历史原因造成的向后兼容性问题,HTTP/1.1只在请求中包含绝对路径,把主机名放入单独的标题域中; - - - -#### RTSP和RTP的关系 - -RTP不象http和ftp可完整的下载整个影视文件,它是以固定的数据率在网络上发送数据,客户端也是按照这种速度观看影视文件,当影视画面播放过后,就不可以再重复播放,除非重新向服务器端要求数据 - -RTSP与RTP最大的区别在于:RTSP是一种双向实时数据传输协议,是纯粹的传输控制协议,它允许客户端向服务器端发送请求,如回放、快进、倒退等操作。当然,RTSP可基于RTP来传送数据,还可以选择TCP、UDP、组播UDP等通道来发送数据,具有很好的扩展性。 - - - - - -![img](https://img-blog.csdn.net/20130925235918343?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdHR0eWQ=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) - - - - - - - -RTMP ---- - -Real Time Messaging Protocol(实时消息传送协议)基于FLV格式进行开发,是Adobe公司为Flash播放器和服务器之间音频、视频和数据传输开发的一种应用层的协议,用来解决多媒体数据传输流的多路复用(Multiplexing)和分包(packetizing)的问题。在基于传输层协议的链接建立完成后,RTMP协议也要客户端和服务器通过“握手”来建立基于传输层链接之上的RTMP Connection链接。协议基于TCP,是一个协议族(默认端口1935),包括RTMP基本协议及RTMPT/RTMPS/RTMPE等多种变种。市面上绝大部分PC秀场使用的都是它,他有低延迟(2s左右)、稳定性高、技术完善、高支持度、编码兼容性高等特点。但是RTMP协议不使用标准的HTTP接口传输数据(TCP、UDP端口),所以在一些特殊的网络环境下可能被防火墙屏蔽掉。 - -使用RTMP技术的流媒体系统有一个非常明显的特点:使用 Flash Player作为播放器客户端,而Flash Player 现在已经安装在了全世界将近99%的PC上,因此一般情况下收看RTMP流媒体系统的视音频是不需要安装插件的。用户只需要打开网页,就可以直接收看流媒体,十分方便。 - -采用RTMP协议时,从采集推流端到流媒体服务器再到播放端是一条数据流,因此在服务器不会有落地文件。这样RTMP相对来说就有这些优点: - -- 实时性高:一般能做到3秒内。 -- 基于 TCP 长连接,不需要多次建连。 - -缺点就是:很多防火墙会墙掉RTMP,但是不会墙掉HTTP。 - - - -Flash Video:是Adobe公司推出的另一种视频格式,是一种在网络上传输的流媒体数据存储容器格式。其格式相对简单轻量,不需要很大的媒体头部信息。整个FLV由Header和Body以及其他Tag组成。因此加载速度极快。 - - - - -HTTP FLV ---- -类似RTMP流式协议的HTTP长连接,需由特定流媒体服务器分发的,是真正的HTTP流媒体传输方式,他在延时、首画等体验上跟RTMP等流式协议拥有完全一致的表现,同时继承了部分HTTP的优势。 - -http+flv ,将音视频数据封装成FLV格式,然后通过HTTP协议传输给客户端。http_flv&rtmp这两个协议实际上传输数据是一样的,数据都是flv文件的tag。 - - - - HTTP协议中有个约定:content-length字段,用于描述HTTP消息实体的传输长度。服务器回复http请求的时候如果有这个字段,客户端就接收这个长度的数据然后就认为数据传输完成了,如果服务器回复http请求中没有这个字段,客户端就一直接收数据,直到服务器跟客户端的socket连接断开。http-flv直播就是利用第二个原理,服务器回复客户端请求的时候不加content-length字段,在回复了http内容之后,紧接着发送flv数据,客户端就一直接收数据了,http-flv这种流,服务器是不可能预先知道内容大小的,这时就可以使用Transfer-Encoding:chunk模式来传输数据了。 - - 相比RTMP,HTTP-FLV会生成一个非常大的http流,只能做拉流,RTMP可以做推流/拉流. - -![](https://raw.githubusercontent.com/CharonChui/Pictures/master/vs_no_background1.png) - - - -常用的流媒体协议主要有HTTP渐进式下载和基于RTSP/RTP的实时流媒体协议栈等等,这些流媒体协议⼤大多数可以平移到移动流媒体中继续应⽤用。然⽽而由于移动互联⽹网及其终端设备的⼀一些独有特性,传统流媒体协议在移动互联⽹网中的应⽤用在功能、性能的提供和⽤用户体验等⽅方⾯面都会受到不同程度的约束和限制,于是一些新的流媒体协议应运⽽而⽣生。例如,苹果公司的 HTTP Live Streaming 就是其中具有代表性且得到较为⼴广泛应⽤用的一个。 但是HLS(Apple)、HDS(Adobe)、MSS(Microsoft) 、DASH(MPEG组织)均属于“伪”HTTP流,之所以说他们“伪”,是因为他们在体验上类似“流”,但本质上依然是HTTP文件下载。以上几个协议的原理都一样,就是将媒体数据(文件或者直播信号)进行切割分块,同时建立一个分块对应的索引表,一并存储在HTTP Web服务器中,客户端连续线性的请求这些分块小文件,以HTTP文件方式下载,顺序的进行解码播放,我们就得到了平滑无缝的“流”的体验。 - - - -•RTSP/RTP 协议栈的逻辑实现较为复杂,与 HTTP 相⽐比⽀支持 RTSP/RTP 的客户端软硬件实现难度较⼤大,特别是对于嵌⼊入式移动设备终端来说。•RT S P 协议使⽤用的⽹网络端⼜⼝口号(554)可能被部分⽤用户⽹网络中的防⽕火墙和NAT等封堵,导致⽆无法使⽤用。虽然有些流媒体服务器可通过隧道⽅方式将 RTSP 配置在HTTP 的 80 端⼜⼝口上承载,但实际部署起来并不是特别⽅方便。HTTP Live Streaming 正是为了解决这些问题应运⽽而⽣生的,其主要特点是:放弃专门的流媒体服务器,⽽而返回到使⽤用标准的 Web 服务器来递送媒体数据;将容量巨⼤大的连续媒体数据进⾏行分段,分割为数量众多的⼩小⽂文件进⾏行传递,迎合了 Web 服务器的⽂文件传输特性;采⽤用了⼀一个不断更新的轻量级索引⽂文件来控制分割后⼩小媒体⽂文件的下载和播放,可同时⽀支持直播和点播,以及 VCR 类会话控制操作。HTTP 协议的使⽤用降低了HTTP Live Streaming 系统的部署难度,同时也简化了客户端(特别是嵌⼊入式移动终端)软件的开发复杂度。此外,⽂文件分割和索引⽂文件的引⼊入也使得带宽⾃自适应的流间切换、服务器故障保护和媒体加密等变得更加⽅方便。与 RTSP/RTP 相⽐比,HTTP Live Streaming 的最⼤大缺点在于它并⾮非⼀一个真正的实时流媒体系统,在服务器和客户端都存在⼀一定的起始延迟。 - - - - -HLS ---- -HTTP Live Streaming:是苹果公司实现的基于HTTP的流媒体传输协议,可实现流媒体的直播和点播,HLS点播,基本上就是常见的分段HTTP点播,不同在于,它的分段非常小。相对于常见的流媒体直播协议,例如RTMP协议、RTSP协议等,HLS直播最大的不同在于,直播客户端获取到的,并不是一个完整的数据流。HLS协议在服务器端将直播数据流存储为连续的、很短时长的媒体文件(MPEG-TS格式),而客户端则不断的下载并播放这些小文件。因为服务器端总是会将最新的直播数据生成新的小文件,这样客户端只要不停的按顺序播放从服务器获取到的文件,就实现了直播。由此可见,基本上可以认为,HLS是以点播的技术方式来实现直播。由于数据通过HTTP协议传输,所以完全不用考虑防火墙或者代理的问题,而且分段文件的时长很短,客户端可以很快的选择和切换码率,以适应不同带宽条件下的播放。不过HLS的这种技术特点,决定了它的延迟一般总是会高于普通的流媒体直播协议。它也解决了RTMP协议存在的一些问题,例如RTMP协议不使用标准的HTTP接口传输数据(TCP、UDP端口),所以在一些特殊的网络环境下可能被防火墙屏蔽掉,而HLS使用的是HTTP协议传输数据(80端口),不会遇到被防火墙屏蔽的情况。 - -HLS相比于http-flv的优点就是不需要任何插件,html5可以直播播放,safari,EDGE等浏览器都可以直接播放HLS的视频流。 - -HLS requires that video is packaged in the M2TS transport stream; a format that was designed for broadcast TV rather than for streaming content over the internet. Conversely, both DASH and SmoothStreaming use the fragmented MP4 (fMP4) container format, which was designed specifically with streaming content over the internet in mind. M2TS has many inherent disadvantages compared to fMP4, both for servers and clients, some of which are summarized by Timothy Siglin’s excellent [white paper](http://download.kennisportal.com/KP/Adobe/UnifyingGlobalVideoStrategies.pdf). - -![](https://raw.githubusercontent.com/CharonChui/Pictures/master/hls_suport_use.png) - - - -在开始一个流媒体会话时,客户端会下载一个包含元数据的extended M3U(m3u8) playlist文件,用于寻找可用的媒体流(将视频分为一个个视频小分片,然后用m3u8索引表进行管理,由于客户端下载到的视频都是5-10秒的完整数据,所以视频的流畅性很好,但是同样也引入了很大的延迟(一般延迟在10-30s左右))。 - -上面提到了m3u8,那m3u8构成是?直播中m3u8、ts如何实时更新? - -- 是一个索引地址/播放列表,通过FFmpeg将本地的xxx.mp4进行切片处理,生成m3u8播放列表(索引文件)和N多个 .ts文件,并将其(m3u8、N个ts)放置在本地搭建好的webServer服务器的指定目录下,我就可以得到一个可以实时播放的网址,我们把这个m3u8地址复制到 VLC 上就可以实时观看! - 在 HLS 流下,本地视频被分割成一个一个的小切片,一般10秒一个,这些个小切片被 m3u8管理,并且随着终端的FFmpeg 向本地拉流的命令而实时更新,影片进度随着拉流的进度而更新,播放过的片段不在本地保存,自动删除,直到该文件播放完毕或停止,ts 切片会相应的被删除,流停止,影片不会立即停止,影片播放会滞后于拉流一段时间 - - - -HLS的整体流程为: - -视频源文件 -> 服务器(编码器、流分割器) -> 分发器(索引文件、*.ts) -> 传输网络 -> 客户端 - - - -[HLS的详细介绍](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) - -HDS ---- -`Http Dynamic Streaming`:是一个由Adobe公司模仿HLS协议提出的另一个基于Http的流媒体传输协议(传统流媒体解决方案RTMP+FLV的组合)。其模式与HLS类似,也是索引文件和媒体切片文件结合的下载方式,但是又要比HLS协议更复杂。 - -Smooth Streaming ---- - -微软提供的一套解决方案,是IIS的媒体服务扩张,用于支持基于HTTP的自适应串流,它的文件切片为mp4,索引文件为ism/ismc。 - - - -在基于HTTP提供流媒体的方面,到目前为止已经看到了三种方案,苹果的HLS,Adobe HTTP Dynamic Streaming (HDS)和Microsoft Smooth Streaming (MSS),当然,各家用的协议,格式会不一样。于是MPEG呼吁大家做到一起,聊聊出一个统一的标准。 - - - -DASH ---- - -DASH(MPEG-DASH)全称为Dynamic Adaptive Streaming over HTTP。是国标标准组MPEG( (Moving Picture Experts Group) 2014年推出的技术标准,主要目标是形成IP网络承载单一格式的流媒体并提供高效与高质量服务的统一方案,解决多制式传输方案(HTTP Live Streaming, Microsoft Smooth Streaming, HTTP Dynamic Streaming)并存格局下的存储与服务能力浪费、运营高成本与复杂度、系统间互操作弱等问题。 - -DASH是基于HTTP的动态自适应的比特率流技术,使用的传输协议是TCP,也是把视频分割成一小段一小段, 通过HTTP协议进行传输,客户端得到之后进行播放;不同的是MPEG-DASH支持MPEG-2 TS、MP4等多种格式, 可以将视频按照多种编码切割, 下载下来的媒体格式既可以是ts文件也可以是mp4文件, 所以当客户端加载视频时, 按照当前的网速和支持的编码加载相应的视频片段进行播放. - -因为是分片的,客户端可以自由选择需要播放的媒体分片,可以实现`Adaptive Bitrate Streaming`技术,不同画质内容无缝切换,提供更好的播放体验。 - -YouTube采用DASH!其网页端及移动端APP都使用了DASH。 - -DASH的整个流程: - -内容生成服务器(编码模块、封装模块) -> 流媒体服务器(MPD、媒体文件、HTTP服务器) <-> DASH客户端(控制引擎、媒体引擎、HTTP接入容器) - - - -[DASH详细介绍](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) - - - -![](https://raw.githubusercontent.com/CharonChui/Pictures/master/dash_compare2.png) - - - -![hls_hds_mss_dash](https://raw.githubusercontent.com/CharonChui/Pictures/master/hls_hds_mss_dash.png) - -这么多流媒体协议我该选用哪个?需要说明的是,各种流媒体协议都有其生存的理由,比如监控行业、电信行业IPTV就不能没有RTSP,因为这里面所有的监控应用程序太多基于RTSP;比如目前的直播主协议就是RTMP,主要是因为CDN对RTMP支持的最好;再比如Apple终端市场占有率太高,就不能够不去考虑HLS。 - - 每一种流媒体协议都有其应用场景,每一种流媒体协议都有其发展的历史原因,每一种流媒体协议都有其自身的优势和不足。所以,我们需要对各种协议的原理、构成、特性都有所了解,才能在自身应用场景中选择出最佳方案。 - - - -参考: - -- [MPEG-DASH vs. Apple HLS vs. Microsoft Smooth Streaming vs. Adobe HDS](https://bitmovin.com/mpeg-dash-vs-apple-hls-vs-microsoft-smooth-streaming-vs-adobe-hds/) -- [A Survey on Quality of Experience ofHTTP Adaptive Streaming](http://www.comnet.informatik.uni-wuerzburg.de/publikationen/journal-articles/?tx_extbibsonomycsl_publicationlist%5BuserName%5D=uniwue_info3&tx_extbibsonomycsl_publicationlist%5BintraHash%5D=99067f2f003db1689d6ac3880a8e0c22&tx_extbibsonomycsl_publicationlist%5BfileName%5D=jour_126.pdf&tx_extbibsonomycsl_publicationlist%5Baction%5D=download&tx_extbibsonomycsl_publicationlist%5Bcontroller%5D=Document&cHash=92121b0e4d712ba4ccb918e8a1d82c34) -- [渐进式、HLS、DASH、HDS、RTMP协议](http://www.4u4v.net/liu-mei-ti-xie-yi-hu-lian-wang-shi-pin-fen-fa-xie-yi-jie-shao-jian-jin-shi-hlsdashhdsrtmp-xie-yi.html) -- [Real-Time Messaging Protocol](https://en.wikipedia.org/wiki/Real-Time_Messaging_Protocol) -- [HTTP Live Streaming](https://en.wikipedia.org/wiki/HTTP_Live_Streaming) -- [Streaming Protocols: Everything You Need to Know](https://www.wowza.com/blog/streaming-protocols) - - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file 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 a404777d..8cf22ef6 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,15 +3,97 @@ FLV +FLV封装格式是由一个文件头(FLV header)和很多tag组成(FLV body)组成的二进制文件。Tag中包含了音频数据以及视频数据。FLV的结构如下图所示。 +![img](https://img-blog.csdn.net/20160118103525777) +tag又可以分成三类:audio,video,script,分别代表音频流,视频流,脚本流,而每个tag又由tag header和tag data组成。 +#### FLV整体结构图: +![img](https:////upload-images.jianshu.io/upload_images/9078032-4d1e3f09df181782.png?imageMogr2/auto-orient/strip|imageView2/2/w/843) +#### FLV文件头结构图 + + + +![img](https:////upload-images.jianshu.io/upload_images/9078032-b0bab07d69f55262.png?imageMogr2/auto-orient/strip|imageView2/2/w/624) + + + +​ FLV文件头由9bytes组成,前3个bytes是文件类型,总是“FLV”,也就是(0x46 0x4C 0x56)。第4btye是版本号,目前一般是0x01。第5byte是流的信息,倒数第一bit是1表示有视频(0x01),倒数第三bit是1表示有音频(0x4),有视频又有音频就是0x01 | 0x04(0x05),其他都应该是0。最后4bytes表示FLV 头的长度,3+1+1+4 = 9。 + +2、 FLV body结构分析 + +​ FLV body由若干个tag 组成。每一个tag第一部分是tag header,tag header长度为11bytes,但是每个tag header前面有4bytes记录着上一个tag的长度。 + +​ tag结构图: + +![img](https:////upload-images.jianshu.io/upload_images/9078032-24c834de3b517f60.png?imageMogr2/auto-orient/strip|imageView2/2/w/853) + +​ tag header: + +​ 1)第1个byte为记录着tag的类型,音频(0x8),视频(0x9),脚本(0x12); + +​ 2)第2到4bytes是数据区的长度,也就是tag data的长度; + +​ 3)再后面3个bytes是时间戳,单位是毫秒,类型为0x12则时间戳为0,时间戳控制着文件播放的速度,可以根据音视频的帧率类设置; + +​ 4)时间戳后面一个byte是扩展时间戳,时间戳不够长的时候用; + +​ 5)最后3bytes是streamID,但是总为0,再后面就是数据区了(tag data),也即是h264的裸流; + +​ 6)tag header 长度为1+3+3+1+3=11。 + +​ 音频TagData结构分析: + +![img](https:////upload-images.jianshu.io/upload_images/9078032-2339809cce2f8ab0.png?imageMogr2/auto-orient/strip|imageView2/2/w/852) + +​ 音频参数中各字段的值及其意义如下表所示: + +![img](https:////upload-images.jianshu.io/upload_images/9078032-7265d2aa76864647.png?imageMogr2/auto-orient/strip|imageView2/2/w/654) + + 音频参数对照表 + +​ 视频TagData结构: + +![img](https:////upload-images.jianshu.io/upload_images/9078032-78db278c8115b2a8.png?imageMogr2/auto-orient/strip|imageView2/2/w/851) + + + +​ Script TagData结构 + +​ Script Tag通常被称为Metadata Tag,会放一些关于FLV视频和音频的元数据信息如:duration、width、height等。通常此类型Tag会跟在File Header后面作为第一个Tag出现,而且只有一个。 + +![img](https:////upload-images.jianshu.io/upload_images/9078032-52b10dcecd85efe9.png?imageMogr2/auto-orient/strip|imageView2/2/w/843) + + + +​ 第一个AMF包: + +​ 第1个字节表示AMF包类型,一般总是0x02,表示字符串。第2-3个字节为UI16类型值,标识字符串的长度,一般总是 0x000A(“onMetaData”长度)。后面字节为具体的字符串,一般 为“onMetaData”(6F,6E,4D,65,74,61,44,61,74,61)。 + +所以第一个AMF包总共占13字节。 + +​ 第二个AMF包结构图: + +![img](https:////upload-images.jianshu.io/upload_images/9078032-023c79ab3f1c9f83.png?imageMogr2/auto-orient/strip|imageView2/2/w/842) + + 第二个AMF包结构图 + +​ 第1个字节表示AMF包类型,一般总是0x08,表示数组。第2-5个字节为UI32类型值,表示数组元素的个数,后面即为各数组元素的封装。数组元素为元素名称和值组成的对。“数组元素结构”部分是推测,已经确认适用于duration、width、height等常见元素,但并不确认适用于所有元素。常见的数组元素如下表所示。 + +![img](https:////upload-images.jianshu.io/upload_images/9078032-ca0f9296f78d19e6?imageMogr2/auto-orient/strip|imageView2/2/w/407) + + + +参考: + +- [flv/video_file_format_spec_v10](https://www.adobe.com/content/dam/acom/en/devnet/flv/video_file_format_spec_v10.pdf) 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 aa7cef68..f5021cd2 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" @@ -7,9 +7,7 @@ MP4的格式稍微比FLV复杂一些,它是通过嵌的方式来实现整个 - - -P4视频文件封装格式是基于QuickTime容器格式定义的,因此参考QuickTime的格式定义对理解MP4文件格式很有帮助。MP4文件格式是一个十分开放的容器,几乎可以用来描述所有的媒体结构,MP4文件中的媒体描述与媒体数据是分开的,并且媒体数据的组织也很自由,不一定要按照时间顺序排列,甚至媒体数据可以直接引用其他文件。同时,MP4也支持流媒体。MP4目前被广泛用于封装h.264视频和AAC音频,是高清视频的代表。 +MP4视频文件封装格式是基于QuickTime容器格式定义的,因此参考QuickTime的格式定义对理解MP4文件格式很有帮助。MP4文件格式是一个十分开放的容器,几乎可以用来描述所有的媒体结构,MP4文件中的媒体描述与媒体数据是分开的,并且媒体数据的组织也很自由,不一定要按照时间顺序排列,甚至媒体数据可以直接引用其他文件。同时,MP4也支持流媒体。MP4目前被广泛用于封装h.264视频和AAC音频,是高清视频的代表。 @@ -22,10 +20,6 @@ P4视频文件封装格式是基于QuickTime容器格式定义的,因此参考 - - - - MP4文件由若干称为Atom(或称为box)的数据对象组成,每个Atom的起首为四个字节的数据长度(Big Endian)和四个字节的类型标识,数据长度和类型标志都可以扩展,Atom的基本结构是: ``` [4bytes atom size] [4bytes atom type] [8bytes largesize, if size ==1] [contents of the atom, if any] @@ -110,36 +104,6 @@ mvhd定义了整个movie的特性,通常包含媒体无关的信息,例如 - - - - - - -https://blog.csdn.net/qq_19923217/article/details/95049837 - - - -https://www.villainhr.com/page/2017/08/21/%E5%AD%A6%E5%A5%BD%20MP4%EF%BC%8C%E8%AE%A9%E7%9B%B4%E6%92%AD%E6%9B%B4%E7%BB%99%E5%8A%9B#fragmented%20MP4 - - - - - - - - - - - - - - - - - - - --- - 邮箱 :charon.chui@gmail.com diff --git "a/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/TS.md" "b/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/TS.md" index 451f6787..e258a1a9 100644 --- "a/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/TS.md" +++ "b/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/TS.md" @@ -3,36 +3,27 @@ TS - - -https://blog.csdn.net/qq_19923217/article/details/94200971 - - - - - TS 全称是 MPEG2-TS,MPEG2-TS 是一种标准容器格式,传输与存储音视频、节目与系统信息协议数据,广泛应用于数字广播系统,我们日常数字机顶盒接收到的就是 TS(Transport Stream,传输流)流。 首先需要先分辨 TS 传输流中几个基本概念 -ES(Elementary Stream):基本流,直接从编码器出来的数据流,可以是编码过的音频、视频或其他连续码流 -PES(Packetized Elementary Streams):PES 流是 ES 流经过 PES 打包器处理后形成的数据流,在这个过程中完成了将 ES 流分组、加入包头信息 (PTS、DTS 等)操作。PES 流的基本单位是 PES 包,PES 包由包头和 payload 组成 -PS 流(Program Stream):节目流,PS 流由 PS 包组成,而一个 PS 包又由若干个 PES 包组成。一个 PS 包由具有同一时间基准的一个或多个 PES 包复合合成。 -TS 流(Transport Stream):传输流,TS 流由固定长度(188 字节)的 TS 包组成,TS 包是对 PES 包的另一种封装方式,同样由具有同一时间基准的一个或多个 PES 包复合合成。PS 包是不固定长度,而 TS 包为固定长度 - - - - + ES(Elementary Stream):基本流,直接从编码器出来的数据流,可以是编码过的音频、视频或其他连续码流 + PES(Packetized Elementary Streams):PES 流是 ES 流经过 PES 打包器处理后形成的数据流,在这个过程中完成了将 ES 流分组、加入包头信息 (PTS、DTS 等)操作。PES 流的基本单位是 PES 包,PES 包由包头和 payload 组成 + PS 流(Program Stream):节目流,PS 流由 PS 包组成,而一个 PS 包又由若干个 PES 包组成。一个 PS 包由具有同一时间基准的一个或多个 PES 包复合合成。 + TS 流(Transport Stream):传输流,TS 流由固定长度(188 字节)的 TS 包组成,TS 包是对 PES 包的另一种封装方式,同样由具有同一时间基准的一个或多个 PES 包复合合成。PS 包是不固定长度,而 TS 包为固定长度。 + + 为便于传输,实现时分复用,基本流 ES 必须打包,就是将顺序连续、连续传输的数据流按一定的时间长度进行分割,分割的小段叫做包,因此打包也被称为分组。 MPEG-2 标准中,有两种不同的码流可以输出到信号,一种是节目码流(PS Program Stream),一种是传输流(TS Transport Stream)。 PS 流包结构长度可变,一旦某一 PS 包的同步信息丢失,接收机就无法确认下一包的同步位置,导致信息丢失,因此 PS 流适用于合理可靠的媒体,如光盘(DVD),PS 流的后缀名一般为 vob 或 evo。而 TS 传输流不同,TS 流的包结构为固定长度(一般为 188 字节),当传输误码破坏了某一 TS 包的同步信息时,接收机可在固定的位置检测它后面包中的同步信息,从而恢复同步,避免信息丢失,因此 TS 可适用于不太可靠的传输,即地面或卫星传播,TS 流的后缀一般为 ts、mpg、mpeg。 + 由于 TS 码流具有较强的抵抗传输误码的能力,因此目前在传输媒体中进行传输的 MPEG-2 码流基本上都采用了 TS +2 基本流程 +2.1 TS 流形成过程 -由于 TS 码流具有较强的抵抗传输误码的能力,因此目前在传输媒体中进行传输的 MPEG-2 码流基本上都采用了 TS - - +image 以电视数字信号为例: 1) 原始音视频数据经过压缩编码得到基本流 ES 流 @@ -49,10 +40,29 @@ PES 包的长度通常都是远大于 TS 包的长度,一个 PES 包必须由 将 PES 包内容分配到一系列固定长度的传输包(TS Packet)中。TS 流中 TS 传输包头加入了 PCR(节目参考时钟)与 PSI(节目专用信息),其中 PCR 用于解码器的系统时钟恢复。 +image + + PCR 时钟作用:我们知道,编码器中有一个系统时钟,用于产生指示音视频正确显示和解码的时间标签(DTS、PTS)。解码器在解码时首先利用 PCR 时钟重建与编码器同步的系统时钟,再利用 PES 流中的 DTS、PTS 进行音视频的同步。 + +4) 连续输出传输包形成具有恒定比特率的 MPEG-TS 流 +2.2 TS 流解析过程 +1) 从复用的 MPEG-TS 流中解析出 TS 包 +2) 从 TS 包中获取 PAT 及节目对应的 PMT,解析获取音视频 + +首先简单了解一下什么是 PSI,后面会通过例子更详细的介绍。 +PSI 是节目特定信息,该表格信息用来描述传送流的组成结构。PSI 信息由四种类型的表组成,包括节目关联表(PAT,Program Association Table)、节目映射表(PMT,Program Map Table)、条件接收表(CAT)、网络信息表(NIT)。PAT 与 PMT 两张表帮助我们找到该传送流中的所有节目与流,PAT 告诉我们,TS 流是由哪些节目组成,每个节目的节目映射表 PMT 的 PID 是什么,而 PMT 告诉我们,该节目由哪些流组成,每一路流的类型与 PID 是什么。CAT 与 NIT 暂时不考虑。 +image +从图中 PAT 表中可以获取该 TS 流中包含哪些节目,并通过 PAT 表中具体节目的 PMT 表 PID 值(如节目 0 对应 17 PMT PID),找到该节目对应的 PMT 表,而有了 PMT 表我们就知道该节目有哪些流以及流的类型(视频、音频等),进而获取到音视频流对应的 PID。 +3) 通过 PID 筛选出特定音视频流的 TS 包,并解析出 PES +4) 从 PES 中读取到 PTS/DTS,并从 PES 中解析出基本码流 ES +5) 将 ES 交给解码器解码 +3 TS 格式 +3.1 TS 包格式 +TS 包主要由两部分组成,一个是 4 字节的包头信息,二是有效负载,另外由于每个包固定需要 188 字节,所以中间有可能需要插入自适应调整字段。其中有效负载包括 PSI(节目专用信息)、PES(打包后的基本流)及其他业务信息。 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 f39c7036..1e1017b1 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" @@ -55,40 +55,12 @@ CENC使用的就是fMP4格式,这是利用了fMP4中音视频可以不复用 - - -MPEG-TS is designed for live streaming of events over DVB, UDP multicast, but also over HTTP. It divides the stream in elementary streams, which are segmented in small chunks. System information is sent at regular intervals, so the receiver can start playing the stream any time. - -MPEG-TS isn't good for streaming files, because it doesn't provide info about the duration of the movie or song, as well as the points you can seek to. - - - ## 主要说了以下几点: - .ts文件不提供关于时长等信息,你无法在ts文件里去实现音视频的seek操作 - .mp4不同于ts,是提供了时长等信息,可以执行seek到指定位置 - .ts文件一般用于m3u8中, 或者提供了流媒体基础信息的前提下使用 -- .mp4文件可以在不下载完全媒体文件的前提下进行seek操作;因为其头部记录moov信息(`moov box 中包含编码、分辨率、码率、帧率、时长、音频采样率等等媒体信息`) - - - - - -https://www.itdaan.com/blog/2018/06/09/8e4ec0afb362459fc6abe8112e82a789.html - - - - - - - - - - - - - - +- .mp4文件可以在不下载完全媒体文件的前提下进行seek操作;因为其头部记录moov信息(moov box 中包含编码、分辨率、码率、帧率、时长、音频采样率等等媒体信息) diff --git "a/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/fMP4\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/fMP4\346\240\274\345\274\217\350\257\246\350\247\243.md" index 232f8b0e..539663d5 100644 --- "a/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/fMP4\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/fMP4\346\240\274\345\274\217\350\257\246\350\247\243.md" @@ -14,41 +14,43 @@ fmp4 是基于 MPEG-4 Part 12 的**流媒体格式**。与普通MP4相比: -![img](https://bitmovin.com/wp-content/uploads/2019/07/image7.png) -https://bitmovin.com/wp-content/uploads/2019/07/image7.png +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/mp4_container_format.webp?raw=true) +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/fmp4_format.png?raw=true) +Fragmented MP4 以 Fragment 的方式存储视频信息。每个 Fragment 由一个 moof box 和一个 mdat box 组成: +- ‘mdat’(media data box) -![](https://upload-images.jianshu.io/upload_images/9570401-03569f6101ecfeea.png?imageMogr2/auto-orient/strip|imageView2/2/w/520) + 和普通MP4文件的‘mdat’一样,用于存放媒体数据,不同的是普通MP4文件只有一个‘mdat’box,而Fragmented MP4文件中,每个fragment都会有一个‘mdat’类型的box。 +- ‘moof’(movie fragment box) + 该类型的box存放的是fragment-level的metadata信息,用于描述所在的fragment。该类型的box在普通的MP4文件中是不存在的,而在Fragmented MP4文件中,每个fragment都会有一个‘moof’类型的box。moof和moov非常像,它包含了当前片段中mp4的相关元信息。 -Fragmented MP4 以 Fragment 的方式存储视频信息。每个 Fragment 由一个 moof box 和一个 mdat box 组成。 +一个‘moof’和一个‘mdat’组成Fragmented MP4文件的一个fragment,这个fragment包含一个video track或audio track,并且包含足够的metadata以保证这部分数据可以单独解码。Fragmented MP4 中的 moov box 只存储文件级别的媒体信息,因此 moov box 的体积比传统的 MP4 中的 moov box 体积要小很多。 -(2)‘mdat’(media data box) -和普通MP4文件的‘mdat’一样,用于存放媒体数据,不同的是普通MP4文件只有一个‘mdat’box,而Fragmented MP4文件中,每个fragment都会有一个‘mdat’类型的box。 -(3)‘moof’(movie fragment box) +![fmp4_parser](https://raw.githubusercontent.com/CharonChui/Pictures/master/fmp4_parser.png?raw=true) -该类型的box存放的是fragment-level的metadata信息,用于描述所在的fragment。该类型的box在普通的MP4文件中是不存在的,而在Fragmented MP4文件中,每个fragment都会有一个‘moof’类型的box。 -一个‘moof’和一个‘mdat’组成Fragmented MP4文件的一个fragment,这个fragment包含一个video track或audio track,并且包含足够的metadata以保证这部分数据可以单独解码。 -A fragment consists of a Movie Fragment Box (moof), which is very similar to a Movie Box (moov). It contains the information about the media streams contained in one single fragment. E.g. it contains the timestamp information for the 10 seconds of video, which are stored in the fragment. Each fragment has its own Media Data (mdat) box. +### FMP4与普通MP4 BOX的区别 +#### Movie Extends Box (mvex)(fMP4专有) +**mvex 是 fMP4 的标准盒子。它的作用是告诉解码器这是一个fMP4的文件,具体的 samples 信息内容不再放到 trak 里面,而是在每一个 moof 中**。基本格式为: -Fragmented MP4 中的 moov box 只存储文件级别的媒体信息,因此 moov box 的体积比传统的 MP4 中的 moov box 体积要小很多。 - +### moof +moof 主要是用来存放 FMP4 的相关内容。它本身没啥太多的内容。 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 79860fd7..612262dc 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" @@ -1,182 +1,101 @@ 视频封装格式 === -音视频组成 -一个完整的视频文件,包括音频、视频和基础元信息,我们常见的视频文件如mp4、mov、flv、avi、rmvb等视频文件,就是一个容器的封装,里面包含了音频和视频两部分,并且都是通过一些特定的编码算法,进行编码压缩过后的。 -H264、Xvid等就是视频编码格式,MP3、AAC等就是音频编码格式。例如:将一个Xvid视频编码文件和一个MP3音频编码文件按AVI封装标准封装以后,就得到一个AVI后缀的视频文件。 -因此,视频转换需要设置的本质就是 - -设置需要的视频编码 -设置需要的音频编码 -选择需要的容器封装 - -一个完整的视频转换设置都至少包括了上面3个步骤。 - - - -编码格式 -音频编码格式 -音频编码格式有如下 - -AAC -AMR -PCM -ogg(ogg vorbis音频) -AC3(DVD 专用音频编码) -DTS(DVD 专用音频编码) -APE(monkey’s 音频) -AU(sun 格式) -WMA - -音频编码方案之间音质比较(AAC,MP3,WMA等)结果: AAC+ > MP3PRO > AAC> RealAudio > WMA > MP3 -目前最常见的音频格式有 Mp3、AC-3、ACC,MP3最广泛的支持最多,AC-3是杜比公司的技术,ACC是MPEG-4中的音频标准,ACC是目前比较先进和具有优势的技术。对应入门,知道有这几种最常见的音频格式足以。 -视频编码格式 -视频编码标准有两大系统: MPEG 和ITU-T,国际上制定视频编解码技术的组织有两个,一个是“国际电联(ITU-T)”,它制定的标准有H.261、H.263、H.263+、H.264等,另一个是“国际标准化组织(ISO)”它制定的标准有MPEG-1、MPEG-2、MPEG-4等。 -常见编码格式有: - -Xvid(MPEG4) -H264 (目前最常用编码格式) -H263 -MPEG1,MPEG2 -AC-1 -RM,RMVB -H.265(目前用的不够多) - -目前最常见的视频编码方式的大致性能排序基本是: MPEG-1/-2 < WMV/7/8 < RM/RMVB < Xvid/Divx < AVC/H.264(由低到高,可能不完全准确)。 -在H.265出来之前,H264是压缩率最高的视频压缩格式,其优势有: - -低码率(Low Bit Rate):和MPEG2和MPEG4 ASP等压缩技术相比,在同等图像质量下,采用H.264技术压缩后的数据量只有MPEG2的1/8,MPEG4的1/3。 -高质量的图象 :H.264能提供连续、流畅的高质量图象(DVD质量)。 -容错能力强 :H.264提供了解决在不稳定网络环境下容易发生的丢包等错误的必要工具。 -网络适应性强 :H.264提供了网络抽象层(Network Abstraction Layer),使得H.264的文件能容易地在不同网络上传输(例如互联网,CDMA,GPRS,WCDMA,CDMA2000等)。 -H.264最大的优势是具有很高的数据压缩比率,在同等图像质量的条件下,H.264的压缩比是MPEG-2的2倍以上,是MPEG-4的1.5~2倍。举个例子,原始文件的大小如果为88GB,采用MPEG-2压缩标准压缩后变成3.5GB,压缩比为25∶1,而采用H.264压缩标准压缩后变为879MB,从88GB到879MB,H.264的压缩比达到惊人的102∶1。低码率(Low Bit Rate)对H.264的高的压缩比起到了重要的作用,和MPEG-2和MPEG-4 ASP等压缩技术相比,H.264压缩技术将大大节省用户的下载时间和数据流量收费。尤其值得一提的是,H.264在具有高压缩比的同时还拥有高质量流畅的图像,正因为如此,经过H.264压缩的视频数据,在网络传输过程中所需要的带宽更少,也更加经济。 -目前这些常见的视频编码格式实际上都属于有损压缩,包括H264和H265,也是有损编码,有损编码才能在质量得以保证的前提下得到更高的压缩率和更小体积 -存储封装格式 -目前市面常见的存储封装格式有如下: - -AVI (.avi) -ASF(.asf) -WMV (.wmv) -QuickTime ( .mov) -MPEG (.mpg / .mpeg) -MP4 (.mp4) -m2ts (.m2ts / .mts ) -Matroska (.mkv / .mks / .mka ) -RM ( .rm / .rmvb) -TS/PS +一个完整的视频文件,包括音频、视频和基础元信息,我们常见的视频文件如mp4、mov、flv、avi、rmvb等视频文件,就是一个容器的封装,里面包含了音频和视频两部分,并且都是通过一些特定的编码算法,进行编码压缩过后的。 +例如:将一个Xvid视频编码文件和一个MP3音频编码文件按AVI封装标准封装以后,就得到一个AVI后缀的视频文件。 -## 常见格式 +## 音频编码格式 -[AVI](https://baike.baidu.com/item/AVI/213655):微软在90年代初创立的封装标准,是当时为对抗quicktime格式(mov)而推出的,只能支持固定[CBR](https://baike.baidu.com/item/CBR/1022793)[恒定比特率](https://baike.baidu.com/item/恒定比特率/5926922)编码的声音文件。 +#### 音频编码格式有如下: -[FLV](https://baike.baidu.com/item/FLV/6623513):针对于[h.263](https://baike.baidu.com/item/h.263/2539746)家族的格式。 +- AAC +- AMR +- PCM +- ogg(ogg vorbis音频) +- AC3(DVD 专用音频编码) +- DTS(DVD 专用音频编码) +- APE(monkey’s 音频) +- AU(sun 格式) +- WMA -MKV:万能封装器,有良好的兼容和跨平台性、纠错性,可带 [外挂字幕](https://baike.baidu.com/item/外挂字幕)。 +音频编码方案之间音质比较(AAC,MP3,WMA等)结果: -MOV:MOV是Quicktime封装。 +AAC+ > MP3PRO > AAC> RealAudio > WMA > MP3 +目前最常见的音频格式有 Mp3、AC-3、ACC,MP3最广泛的支持最多,AC-3是杜比公司的技术,ACC是MPEG-4中的音频标准,ACC是目前比较先进和具有优势的技术。对应入门,知道有这几种最常见的音频格式足以。 -MP4:主要应用于mpeg4的封装 。 +## 视频编码格式 -RM/[RMVB](https://baike.baidu.com/item/RMVB/229903):Real Video,由[RealNetworks](https://baike.baidu.com/item/RealNetworks/1987003)开发的应用于[rmvb](https://baike.baidu.com/item/rmvb/229903)和rm 。 +视频编码标准有两大系统: -TS/PS:PS封装只能在HDDVD原版。 +- “国际电联(ITU-T)”,它制定的标准有H.261、H.263、H.263+、H.264等, +- “国际标准化组织(ISO)”它制定的标准有MPEG-1、MPEG-2、MPEG-4等。 -[WMV](https://baike.baidu.com/item/WMV/1195900):微软推出的,作为市场竞争。 +#### 常见编码格式有: +- Xvid(MPEG4) +- H265 -Q:[b]为什么把flv叫做流式文件格式? 和mp4,avi不是一样都是音视频的容器吗? 有什么区别?[/b] -一下是我收集的几种解释,每个人有不同的理解,把这些都看一遍,你会理解的更加清晰 +- H264 -[quote]通常说的流式文件是可以边传边解的,开始不需要整个文件。特点是有文件头信息(这个不是必需的)和中间打包了,可以直接解析分包,而且文件可以任意大小,而不需要通过索引分包。FLV,MPEG,RMVB等都可以直接依次分包解析,而MP4,AVI一定要依赖索引表才行,而且开始就要固定位置好,如果索引表在尾部,还没办法解析。[/quote] +- H263 +- MPEG1,MPEG2 -[quote]流媒体文件是指多媒体文件边下载可以边观看的文件。而传统的视频文件需下载完成才能观看,而流媒体主要是下载一部分文件到缓存区,然后再从缓存区里面拿数据~而能作为这种流媒体文件的只有经过特殊编码的格式才适合,而flv、rmvb、mov、asf等格式文件才属于流媒体格式文件~[/quote] +- AC-1 -[quote]对于HTTP协议,流式文件可以使用HTTP分段下载,由于在前面的先播放,所以可以一边下载一边播放,但是对于容器格式的文件,由于客户端不知道如何对文件解析(必须拿到整个文件才能解析),所以不能边下载边播放。 -要实现对容器格式的文件的在线播放,必须要服务器支持流式播放接口,例如RTSP协议[/quote] +- RM,RMVB + +目前最常见的视频编码方式的大致性能排序基本是: + MPEG-1/-2 < WMV/7/8 < RM/RMVB < Xvid/Divx < AVC/H.264(由低到高,可能不完全准确)。 +在H.265出来之前,H264是压缩率最高的视频压缩格式,其优势有: -``` -1、mkv:mkv不等同于音频或视频编码格式,它只是为这些进行过音视频编码的数据提供了一个封装的格式,简单的说就是指定音视频数据在文件中如何排列放置。 -MKV最大的特点就是能容纳多种不同类型编码的视频、音频及字幕流,俗称万能媒体容器。 -MKV加入AVI所没有的EDC错误检测代码,这意味着即使是没有下载完毕的MKV文件也可以顺利回放,这些对AVI来说完全是不可想象的。虽然MKV加入了错误检测代码,但由于采用了新的更高效的组织结构,用MKV封装后的电影还是比AVI源文件要小了约1%,这就是说即使加上了多个字幕,MKV文件的体积也不可能比AVI文件大。 -MKV支持可变帧率,它可在动态画面中使用较大的帧率,而在静态画面中使用较小的帧率,这样可以有效的减少视频文件的体积,并改善动态画面的质量。它的作用比目前广泛使用的VBR(可变码率)更为明显。 +- 低码率(Low Bit Rate):和MPEG2和MPEG4 ASP等压缩技术相比,在同等图像质量下,采用H.264技术压缩后的数据量只有MPEG2的1/8,MPEG4的1/3。 +- 高质量的图象 :H.264能提供连续、流畅的高质量图象(DVD质量)。 +- 容错能力强 :H.264提供了解决在不稳定网络环境下容易发生的丢包等错误的必要工具。 +- 网络适应性强 :H.264提供了网络抽象层(Network Abstraction Layer),使得H.264的文件能容易地在不同网络上传输(例如互联网,CDMA,GPRS,WCDMA,CDMA2000等)。 -2、avi 可容纳多种类型的音频和视频流,他的封装格式比较老了,在功能上不能像mkv那样满足更多的需求 +H.264最大的优势是具有很高的数据压缩比率,在同等图像质量的条件下,H.264的压缩比是MPEG-2的2倍以上,是MPEG-4的1.5~2倍。举个例子,原始文件的大小如果为88GB,采用MPEG-2压缩标准压缩后变成3.5GB,压缩比为25∶1,而采用H.264压缩标准压缩后变为879MB,从88GB到879MB,H.264的压缩比达到惊人的102∶1。低码率(Low Bit Rate)对H.264的高的压缩比起到了重要的作用,和MPEG-2和MPEG-4 ASP等压缩技术相比,H.264压缩技术将大大节省用户的下载时间和数据流量收费。尤其值得一提的是,H.264在具有高压缩比的同时还拥有高质量流畅的图像,正因为如此,经过H.264压缩的视频数据,在网络传输过程中所需要的带宽更少,也更加经济。 +目前这些常见的视频编码格式实际上都属于有损压缩,包括H264和H265,也是有损编码,有损编码才能在质量得以保证的前提下得到更高的压缩率和更小体积。 -3、rmvb 是rm的升级版本,vb代表变比特率,意思是在画面平缓的时候采用低比特率,画面变化剧烈的时候采用高比特率,有效降低文件尺寸,又不影响太多画质。一般来说,一个700MB的 DVDrip 采用平均比特率为450Kbps的压缩率,生成的 RMVB 大小仅为400MB,但是画质并没有太大变化。但是由于编码器的关系,在画质上还是略输于h.264,所以现在压缩高清视频时更偏重于使用mkv封装。 +## 存储封装格式 +目前市面常见的存储封装格式有如下: -4、mp4 视频MP4格式实际上指的是使用MPEG-4编码格式、或使用MPEG-4衍生出来的编码格式进行编码的文件,比如DivX、XviD、H.263、H.264、 MS MPEG-4 3688 、 Microsoft Video1 、Microsoft RLE,此种文件格式功能不如mkv丰富。 +- AVI (.avi) +- ASF(.asf) +- WMV (.wmv) +- QuickTime ( .mov) +- MPEG (.mpg / .mpeg) +- MP4 (.mp4) +- m2ts (.m2ts / .mts ) +- Matroska (.mkv / .mks / .mka ) +- RM ( .rm / .rmvb) +- TS/PS +- FLV -5、flv FLV文件体积小巧,清晰的FLV视频1分钟在1MB左右,一部电影在100MB左右,是普通视频文件体积的1/3。再加上CPU占有率低、视频质量良好等特点使其在网络上盛行,目前网上的几家著名视频共享网站均采用FLV格式文件提供视频 -6、wmv WMV是微软推出的一种流媒体格式,它是在“同门”的ASF(AdvancedStreamFormat)格式升级延伸来得。在同等视频质量下,WMV格式的文件可以边下载边播放,因此很适合在网上播放和传输。 -可是由于微软本身的局限性其WMV的应用发展并不顺利。第一, WM9是微软的产品它必定要依赖着Windows,Windows 意味着解码部分也要有PC,起码要有PC机的主板。这就大大增加了机顶盒的造价,从而影响了视频广播点播的普及。第二,WMV技术的视频传输延迟非常大,通常要10几秒钟,正是由于这种局限性,目前WMV也仅限于在计算机上浏览WM9视频文件。 -``` +- 为什么把flv叫做流式文件格式? 和mp4,avi不是一样都是音视频的容器吗? 有什么区别? + 通常说的流式文件是可以边传边解的,开始不需要整个文件。特点是有文件头信息(这个不是必需的)和中间打包了,可以直接解析分包,而且文件可以任意大小,而不需要通过索引分包。FLV,MPEG,RMVB等都可以直接依次分包解析,而MP4,AVI一定要依赖索引表才行,而且开始就要固定位置好,如果索引表在尾部,还没办法解析。 -对于相同的音视频内容,使用三种不同的封装格式,则文件体积从大到小依次为 + 流媒体文件是指多媒体文件边下载可以边观看的文件。而传统的视频文件需下载完成才能观看,而流媒体主要是下载一部分文件到缓存区,然后再从缓存区里面拿数据~而能作为这种流媒体文件的只有经过特殊编码的格式才适合,而flv、rmvb、mov、asf等格式文件才属于流媒体格式文件 -TS -> MP4 -> FLV +对于相同的音视频内容,使用三种不同的封装格式,则文件体积从大到小依次为: TS -> MP4 -> FLV FLV和MP4封装格式的文件大小基本相等。 -例如:对于同一个文件,采用相同的编码设置,封装为不同的格式 - -[root@localhost ffmpeg-2.1.1]# ffmpeg -i test_wei.flv -t 10 -vcodec libx264 -x264opts keyint=24 -acodec libfaac -r 24 -y output10s.ts - - - - - -[root@localhost ffmpeg-2.1.1]# ffmpeg -i test_wei.flv -t 10 -vcodec libx264 -x264opts keyint=24 -acodec libfaac -r 24 -y output10s.flv - - - -[root@localhost ffmpeg-2.1.1]# ffmpeg -i test_wei.flv -t 10 -vcodec libx264 -x264opts keyint=24 -acodec libfaac -r 24 -y output10s.mp4 - - - - - -[root@localhost ffmpeg-2.1.1]# ll - --rw-r--r-- 1 root root **1354272** Apr 18 11:06 output10s.flv - --rw-r--r-- 1 root root **1355649** Apr 18 11:07 output10s.mp4 - --rw-r--r-- 1 root root **1492156** Apr 18 11:04 output10s.ts - - - - - -## 封装格式与编码方式的对应 - - - -AVI:可用[MPEG-2](https://baike.baidu.com/item/MPEG-2/214322), DIVX, XVID, WMV3, WMV4, WMV9, H.264 - -WMV:可用WMV3, WMV4, WMV9 - -RM/[RMVB](https://baike.baidu.com/item/RMVB/229903):可用RV40, RV50, RV60, RM8, RM9, RM10 - -MOV:可用MPEG-2, MPEG4-ASP([XVID](https://baike.baidu.com/item/XVID/567063)), H.264 - -MKV:可用所有[视频编码](https://baike.baidu.com/item/视频编码)方案 - From 3b91efe8c4c884bdf2c0a7dbdceb0b393e9f6527 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 11 Jun 2020 20:45:07 +0800 Subject: [PATCH 023/213] update readme --- README.md | 40 ++++++++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ae74c648..50ed2ded 100644 --- a/README.md +++ b/README.md @@ -54,20 +54,34 @@ Android学习笔记 - [DLNA简介][24] - [AudioTrack简介][214] - [流媒体协议][224] - - [流媒体通信协议][246] + - [流媒体协议][246] - [HLS][247] - [DASH][248] + - [HTTP FLV][249] + - [RTMP][250] - [ExoPlayer][216] - [1. ExoPlayer简介.md][217] - [2. ExoPlayer MediaSource简介][218] - [3. ExoPlayer源码分析之prepare方法][219] - [4. ExoPlayer源码分析之prepare序列图][220] - [5. ExoPlayer源码分析之PlayerView][221] - - [MP4格式详解][225] + - [视频封装格式][225] + - [MP4格式详解][251] + - [FLV][252] + - [TS][253] + - [fMP4 vs ts][254] + - [fMP4格式详解][255] + - [视频封装格式][256] + - [视频编码][257] + - [AV1][258] + - [H264][259] + - [H265][260] - [SurfaceView与TextureView][226] - [关键帧][227] - [CDN及PCDN][228] - - [P2P][229] + - [P2P技术][229] + - [P2P][261] + - [P2P原理_NAT穿透][262] - [播放器性能优化][230] - [MediaExtractor、MediaCodec、MediaMuxer][245] - [OpenGL][231] @@ -503,11 +517,11 @@ Android学习笔记 [222]: https://github.com/CharonChui/AndroidNote/blob/master/BasicKnowledge/%E5%8F%8D%E7%BC%96%E8%AF%91.md "反编译" [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/blob/master/VideoDevelopment/MP4%E6%A0%BC%E5%BC%8F%E8%AF%A6%E8%A7%A3.md "MP4格式详解" +[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" [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/blob/master/VideoDevelopment/P2P.md "P2P" +[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 "播放器性能优化" [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简介" @@ -525,9 +539,23 @@ Android学习笔记 [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" -[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%E9%80%9A%E4%BF%A1%E5%8D%8F%E8%AE%AE.md "流媒体通信协议" +[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" +[249]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E6%B5%81%E5%AA%92%E4%BD%93%E5%8D%8F%E8%AE%AE/HTTP%20FLV.md "HTTP FLV" +[250]: 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 "RTMP" +[251]: 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/MP4%E6%A0%BC%E5%BC%8F%E8%AF%A6%E8%A7%A3.md "MP4格式详解" +[252]: 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/FLV.md "FLV" +[253]: 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/TS.md "TS" +[254]: 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/fMP4%20vs%20ts.md "fMP4 vs ts" +[255]: 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/fMP4%E6%A0%BC%E5%BC%8F%E8%AF%A6%E8%A7%A3.md "fMP4格式详解" +[256]: 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/%E8%A7%86%E9%A2%91%E5%B0%81%E8%A3%85%E6%A0%BC%E5%BC%8F.md "视频封装格式" +[257]: https://github.com/CharonChui/AndroidNote/tree/master/VideoDevelopment/%E8%A7%86%E9%A2%91%E7%BC%96%E7%A0%81 "视频编码" +[258]:https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E8%A7%86%E9%A2%91%E7%BC%96%E7%A0%81/AV1.md "AV1" +[259]:https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E8%A7%86%E9%A2%91%E7%BC%96%E7%A0%81/H264.md "H264" +[260]:https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E8%A7%86%E9%A2%91%E7%BC%96%E7%A0%81/H265.md "H265" +[261]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/P2P%E6%8A%80%E6%9C%AF/P2P.md "P2P" +[262]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/P2P%E6%8A%80%E6%9C%AF/P2P%E5%8E%9F%E7%90%86_NAT%E7%A9%BF%E9%80%8F.md "P2P原理_NAT穿透" From 5759f3bdffa2a1f43a9c300fbbe142ab51118ae7 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 11 Jun 2020 21:09:31 +0800 Subject: [PATCH 024/213] update --- README.md | 2 +- .../HLS.md" | 10 +- .../FLV.md" | 34 ++++--- .../TS.md" | 65 +------------ ...01\350\243\205\346\240\274\345\274\217.md" | 6 +- .../AV1.md" | 90 +----------------- .../H265.md" | 92 +------------------ 7 files changed, 37 insertions(+), 262 deletions(-) diff --git a/README.md b/README.md index 50ed2ded..766477dd 100644 --- a/README.md +++ b/README.md @@ -543,7 +543,7 @@ Android学习笔记 [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" [249]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E6%B5%81%E5%AA%92%E4%BD%93%E5%8D%8F%E8%AE%AE/HTTP%20FLV.md "HTTP FLV" -[250]: 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 "RTMP" +[250]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E6%B5%81%E5%AA%92%E4%BD%93%E5%8D%8F%E8%AE%AE/RTMP.md "RTMP" [251]: 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/MP4%E6%A0%BC%E5%BC%8F%E8%AF%A6%E8%A7%A3.md "MP4格式详解" [252]: 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/FLV.md "FLV" [253]: 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/TS.md "TS" 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 ed214e94..707b4758 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" @@ -202,11 +202,11 @@ live m3u8文件列表需要不断更新,更新规则: #### 测试地址: -CCTV1高清:http://ivi.bupt.edu.cn/hls/cctv1hd.m3u8 -CCTV3高清:http://ivi.bupt.edu.cn/hls/cctv3hd.m3u8 -CCTV5高清:http://ivi.bupt.edu.cn/hls/cctv5hd.m3u8 -CCTV5+高清:http://ivi.bupt.edu.cn/hls/cctv5phd.m3u8 -CCTV6高清:http://ivi.bupt.edu.cn/hls/cctv6hd.m3u8 +- CCTV1高清:http://ivi.bupt.edu.cn/hls/cctv1hd.m3u8 +- CCTV3高清:http://ivi.bupt.edu.cn/hls/cctv3hd.m3u8 +- CCTV5高清:http://ivi.bupt.edu.cn/hls/cctv5hd.m3u8 +- CCTV5+高清:http://ivi.bupt.edu.cn/hls/cctv5phd.m3u8 +- CCTV6高清:http://ivi.bupt.edu.cn/hls/cctv6hd.m3u8 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 8cf22ef6..15f1f6e5 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" @@ -5,7 +5,9 @@ FLV FLV封装格式是由一个文件头(FLV header)和很多tag组成(FLV body)组成的二进制文件。Tag中包含了音频数据以及视频数据。FLV的结构如下图所示。 -![img](https://img-blog.csdn.net/20160118103525777) +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/flv_tag.jpg?raw=true) + + tag又可以分成三类:audio,video,script,分别代表音频流,视频流,脚本流,而每个tag又由tag header和tag data组成。 @@ -13,15 +15,19 @@ tag又可以分成三类:audio,video,script,分别代表音频流,视频流 #### FLV整体结构图: -![img](https:////upload-images.jianshu.io/upload_images/9078032-4d1e3f09df181782.png?imageMogr2/auto-orient/strip|imageView2/2/w/843) +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/flv_tag.jpg?raw=true) + + + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/flv_header_tag.png?raw=true) #### FLV文件头结构图 - -![img](https:////upload-images.jianshu.io/upload_images/9078032-b0bab07d69f55262.png?imageMogr2/auto-orient/strip|imageView2/2/w/624) + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/flv_tag_2.png?raw=true) @@ -33,7 +39,7 @@ tag又可以分成三类:audio,video,script,分别代表音频流,视频流 ​ tag结构图: -![img](https:////upload-images.jianshu.io/upload_images/9078032-24c834de3b517f60.png?imageMogr2/auto-orient/strip|imageView2/2/w/853) +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/flv_body_tag.png?raw=true) ​ tag header: @@ -51,17 +57,19 @@ tag又可以分成三类:audio,video,script,分别代表音频流,视频流 ​ 音频TagData结构分析: -![img](https:////upload-images.jianshu.io/upload_images/9078032-2339809cce2f8ab0.png?imageMogr2/auto-orient/strip|imageView2/2/w/852) +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/flv_audio_tag.png?raw=true) ​ 音频参数中各字段的值及其意义如下表所示: -![img](https:////upload-images.jianshu.io/upload_images/9078032-7265d2aa76864647.png?imageMogr2/auto-orient/strip|imageView2/2/w/654) + + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/flv_audio_tag2.png?raw=true) 音频参数对照表 ​ 视频TagData结构: -![img](https:////upload-images.jianshu.io/upload_images/9078032-78db278c8115b2a8.png?imageMogr2/auto-orient/strip|imageView2/2/w/851) +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/flv_video_tag.png?raw=true) @@ -69,7 +77,9 @@ tag又可以分成三类:audio,video,script,分别代表音频流,视频流 ​ Script Tag通常被称为Metadata Tag,会放一些关于FLV视频和音频的元数据信息如:duration、width、height等。通常此类型Tag会跟在File Header后面作为第一个Tag出现,而且只有一个。 -![img](https:////upload-images.jianshu.io/upload_images/9078032-52b10dcecd85efe9.png?imageMogr2/auto-orient/strip|imageView2/2/w/843) +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/flv_script_tag.png?raw=true) + + @@ -81,13 +91,15 @@ tag又可以分成三类:audio,video,script,分别代表音频流,视频流 ​ 第二个AMF包结构图: -![img](https:////upload-images.jianshu.io/upload_images/9078032-023c79ab3f1c9f83.png?imageMogr2/auto-orient/strip|imageView2/2/w/842) +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/flv_awf_tag.png?raw=true) 第二个AMF包结构图 ​ 第1个字节表示AMF包类型,一般总是0x08,表示数组。第2-5个字节为UI32类型值,表示数组元素的个数,后面即为各数组元素的封装。数组元素为元素名称和值组成的对。“数组元素结构”部分是推测,已经确认适用于duration、width、height等常见元素,但并不确认适用于所有元素。常见的数组元素如下表所示。 -![img](https:////upload-images.jianshu.io/upload_images/9078032-ca0f9296f78d19e6?imageMogr2/auto-orient/strip|imageView2/2/w/407) +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/flv_amf_tag.jpg?raw=true) + + diff --git "a/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/TS.md" "b/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/TS.md" index e258a1a9..c0a2daad 100644 --- "a/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/TS.md" +++ "b/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/TS.md" @@ -3,70 +3,7 @@ TS -TS 全称是 MPEG2-TS,MPEG2-TS 是一种标准容器格式,传输与存储音视频、节目与系统信息协议数据,广泛应用于数字广播系统,我们日常数字机顶盒接收到的就是 TS(Transport Stream,传输流)流。 - -首先需要先分辨 TS 传输流中几个基本概念 - - ES(Elementary Stream):基本流,直接从编码器出来的数据流,可以是编码过的音频、视频或其他连续码流 - PES(Packetized Elementary Streams):PES 流是 ES 流经过 PES 打包器处理后形成的数据流,在这个过程中完成了将 ES 流分组、加入包头信息 (PTS、DTS 等)操作。PES 流的基本单位是 PES 包,PES 包由包头和 payload 组成 - PS 流(Program Stream):节目流,PS 流由 PS 包组成,而一个 PS 包又由若干个 PES 包组成。一个 PS 包由具有同一时间基准的一个或多个 PES 包复合合成。 - TS 流(Transport Stream):传输流,TS 流由固定长度(188 字节)的 TS 包组成,TS 包是对 PES 包的另一种封装方式,同样由具有同一时间基准的一个或多个 PES 包复合合成。PS 包是不固定长度,而 TS 包为固定长度。 - - 为便于传输,实现时分复用,基本流 ES 必须打包,就是将顺序连续、连续传输的数据流按一定的时间长度进行分割,分割的小段叫做包,因此打包也被称为分组。 - -MPEG-2 标准中,有两种不同的码流可以输出到信号,一种是节目码流(PS Program Stream),一种是传输流(TS Transport Stream)。 - -PS 流包结构长度可变,一旦某一 PS 包的同步信息丢失,接收机就无法确认下一包的同步位置,导致信息丢失,因此 PS 流适用于合理可靠的媒体,如光盘(DVD),PS 流的后缀名一般为 vob 或 evo。而 TS 传输流不同,TS 流的包结构为固定长度(一般为 188 字节),当传输误码破坏了某一 TS 包的同步信息时,接收机可在固定的位置检测它后面包中的同步信息,从而恢复同步,避免信息丢失,因此 TS 可适用于不太可靠的传输,即地面或卫星传播,TS 流的后缀一般为 ts、mpg、mpeg。 - - 由于 TS 码流具有较强的抵抗传输误码的能力,因此目前在传输媒体中进行传输的 MPEG-2 码流基本上都采用了 TS - -2 基本流程 -2.1 TS 流形成过程 - -image - -以电视数字信号为例: -1) 原始音视频数据经过压缩编码得到基本流 ES 流 - -生成的 ES 基本流比较大,并且只是 I、P、B 这些视频帧或音频取样信息。 -2) 对 ES 基本流 进行打包生成 PES 流 - -通过 PES 打包器,首先对 ES 基本流进行分组打包,在每一个包前加上包头就构成了 PES 流的基本单位 —— PES 包,对视频 PES 来说,一般是一帧一个包,音频 PES 一般一个包不超过 64KB。 - -PES 包头信息中加入了 PTS、DTS 信息,用与音视频的同步。 -3) 同一时间基准的 PES 包经过 TS 复用器生成 TS 传输包 - -PES 包的长度通常都是远大于 TS 包的长度,一个 PES 包必须由整数个 TS 包来传送,没装满的 TS 包由填充字节填充。PES 包进行 TS 复用时,往往一个 PES 包会分存到多个 TS 包中 - -将 PES 包内容分配到一系列固定长度的传输包(TS Packet)中。TS 流中 TS 传输包头加入了 PCR(节目参考时钟)与 PSI(节目专用信息),其中 PCR 用于解码器的系统时钟恢复。 - -image - - PCR 时钟作用:我们知道,编码器中有一个系统时钟,用于产生指示音视频正确显示和解码的时间标签(DTS、PTS)。解码器在解码时首先利用 PCR 时钟重建与编码器同步的系统时钟,再利用 PES 流中的 DTS、PTS 进行音视频的同步。 - -4) 连续输出传输包形成具有恒定比特率的 MPEG-TS 流 -2.2 TS 流解析过程 -1) 从复用的 MPEG-TS 流中解析出 TS 包 -2) 从 TS 包中获取 PAT 及节目对应的 PMT,解析获取音视频 - -首先简单了解一下什么是 PSI,后面会通过例子更详细的介绍。 - -PSI 是节目特定信息,该表格信息用来描述传送流的组成结构。PSI 信息由四种类型的表组成,包括节目关联表(PAT,Program Association Table)、节目映射表(PMT,Program Map Table)、条件接收表(CAT)、网络信息表(NIT)。PAT 与 PMT 两张表帮助我们找到该传送流中的所有节目与流,PAT 告诉我们,TS 流是由哪些节目组成,每个节目的节目映射表 PMT 的 PID 是什么,而 PMT 告诉我们,该节目由哪些流组成,每一路流的类型与 PID 是什么。CAT 与 NIT 暂时不考虑。 - -image - -从图中 PAT 表中可以获取该 TS 流中包含哪些节目,并通过 PAT 表中具体节目的 PMT 表 PID 值(如节目 0 对应 17 PMT PID),找到该节目对应的 PMT 表,而有了 PMT 表我们就知道该节目有哪些流以及流的类型(视频、音频等),进而获取到音视频流对应的 PID。 -3) 通过 PID 筛选出特定音视频流的 TS 包,并解析出 PES -4) 从 PES 中读取到 PTS/DTS,并从 PES 中解析出基本码流 ES -5) 将 ES 交给解码器解码 -3 TS 格式 -3.1 TS 包格式 - -TS 包主要由两部分组成,一个是 4 字节的包头信息,二是有效负载,另外由于每个包固定需要 188 字节,所以中间有可能需要插入自适应调整字段。其中有效负载包括 PSI(节目专用信息)、PES(打包后的基本流)及其他业务信息。 - - - - +TODO 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 612262dc..8899f1a8 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" @@ -86,11 +86,11 @@ H.264最大的优势是具有很高的数据压缩比率,在同等图像质量 -- 为什么把flv叫做流式文件格式? 和mp4,avi不是一样都是音视频的容器吗? 有什么区别? +为什么把flv叫做流式文件格式? 和mp4,avi不是一样都是音视频的容器吗? 有什么区别? - 通常说的流式文件是可以边传边解的,开始不需要整个文件。特点是有文件头信息(这个不是必需的)和中间打包了,可以直接解析分包,而且文件可以任意大小,而不需要通过索引分包。FLV,MPEG,RMVB等都可以直接依次分包解析,而MP4,AVI一定要依赖索引表才行,而且开始就要固定位置好,如果索引表在尾部,还没办法解析。 +通常说的流式文件是可以边传边解的,开始不需要整个文件。特点是有文件头信息(这个不是必需的)和中间打包了,可以直接解析分包,而且文件可以任意大小,而不需要通过索引分包。FLV,MPEG,RMVB等都可以直接依次分包解析,而MP4,AVI一定要依赖索引表才行,而且开始就要固定位置好,如果索引表在尾部,还没办法解析。 - 流媒体文件是指多媒体文件边下载可以边观看的文件。而传统的视频文件需下载完成才能观看,而流媒体主要是下载一部分文件到缓存区,然后再从缓存区里面拿数据~而能作为这种流媒体文件的只有经过特殊编码的格式才适合,而flv、rmvb、mov、asf等格式文件才属于流媒体格式文件 +流媒体文件是指多媒体文件边下载可以边观看的文件。而传统的视频文件需下载完成才能观看,而流媒体主要是下载一部分文件到缓存区,然后再从缓存区里面拿数据~而能作为这种流媒体文件的只有经过特殊编码的格式才适合,而flv、rmvb、mov、asf等格式文件才属于流媒体格式文件 对于相同的音视频内容,使用三种不同的封装格式,则文件体积从大到小依次为: TS -> MP4 -> FLV diff --git "a/VideoDevelopment/\350\247\206\351\242\221\347\274\226\347\240\201/AV1.md" "b/VideoDevelopment/\350\247\206\351\242\221\347\274\226\347\240\201/AV1.md" index 45aa49ec..39fb1ead 100644 --- "a/VideoDevelopment/\350\247\206\351\242\221\347\274\226\347\240\201/AV1.md" +++ "b/VideoDevelopment/\350\247\206\351\242\221\347\274\226\347\240\201/AV1.md" @@ -1,97 +1,11 @@ -H264 +AV1 === -H.264是一种高性能的视频编解码技术。目前国际上制定视频编解码技术的组织有两个,一个是“国际电联”,它制定的标准有H.261、H.263、H.263+等,另一个是“国际标准化组织(ISO)”它制定的标准有MPEG-1、MPEG-2、MPEG-4等。而H.264则是由两个组织联合组建的联合视频组(JVT)共同制定的新数字视频编码标准,所以它既是ITU-T的H.264,又是ISO/IEC的MPEG-4高级视频编码,而且它将成为MPEG-4标准的第10部分。因此,不论是MPEG-4 AVC、MPEG-4 Part 10,还是ISO/IEC 14496-10,都是指H.264。 - - - - - -## H.264算法的优势 - - - H.264是在MPEG-4技术的基础之上建立起来的,其编解码流程主要包括5个部分:帧间和帧内预测、变换和反变换、量化和反量化、环路滤波、熵编码。 - - H.264/MPEG-4 AVC(H.264)是1995年自MPEG-2视频压缩标准发布以后的最新、最有前途的视频压缩标准。H.264是由ITU-T和ISO/IEC的联合开发组共同开发的最新国际视频编码标准。通过该标准,在同等图象质量下的压缩效率比以前的标准提高了2倍以上,因此,H.264被普遍认为是最有影响力的行业标准。 - -## H.264的优势 - - - H.264在1997年ITU的视频编码专家组提出时被称为H.26L,在ITU与ISO合作研究后被称为MPEG4 Part10或H.264(JVT)。H.264标准的主要目标是:与其它现有的视频编码标准相比,在相同的带宽下提供更加优秀的图象质量。 - -**而,H.264与以前的国际标准如H.263和MPEG-4相比,最大的优势体现在以下四个方面:** - - - -- 将每个视频帧分离成由像素组成的块,因此视频帧的编码处理的过程可以达到块的级别。 -- 采用空间冗余的方法,对视频帧的一些原始块进行空间预测、转换、优化和熵编码(可变长编码)。 -- 对连续帧的不同块采用临时存放的方法,这样,只需对连续帧中有改变的部分进行编码。该算法采用运动预测和运动补偿来完成。对某些特定的块,在一个或多个已经进行了编码的帧执行搜索来决定块的运动向量,并由此在后面的编码和解码中预测主块。 -- 采用剩余空间冗余技术,对视频帧里的残留块进行编码。例如:对于源块和相应预测块的不同,再次采用转换、优化和熵编码。 - - -**具体优势表现为:** - - - -- 低码流:和MPEG2和MPEG4 ASP等压缩技术相比,在同等图像质量下,采用H.264技术压缩后的数据量只有MPEG2的1/8,MPEG4的1/3。显然,H.264压缩技术的采用将大大节省用户的下载时间和数据流量收费。 -- 高质量的图象:H.264能提供连续、流畅的高质量图象(DVD质量)。 -- 容错能力强:H.264提供了解决在不稳定网络环境下容易发生的丢包等错误的必要工具。 -- 网络适应性强:H.264提供了网络适应层, 使得H.264的文件能容易地在不同网络上传输(例如互联网,CDMA,GPRS,WCDMA,CDMA2000等)。 - - - H.264和以前的标准一样,也是DPCM加变换编码的混合编码模式。但它采用“回归基本”的简洁设计,不用众多的选项,获得比H.263++好得多的压缩性能;加强了对各种信道的适应能力,采用“网络友好”的结构和语法,有利于对误码和丢包的处理;应用目标范围较宽,以满足不同速率、不同解析度以及不同传输(存储)场合的需求。 - -## H.264标准的关键技术 - - - -### 1 帧内预测编码 - - - 帧内编码用来缩减图像的空间冗余。为了提高H.264帧内编码的效率,在给定帧中充分利用相邻宏块的空间相关性,相邻的宏块通常含有相似的属性。因此,在对一给定宏块编码时,首先可以根据周围的宏块预测(典型的是根据左上角的宏块,因为此宏块已经被编码处理),然后对预测值与实际值的差值进行编码,这样,相对于直接对该帧编码而言,可以大大减小码率。 - - - -### 2帧间预测编码 - - - 帧间预测编码利用连续帧中的时间冗余来进行运动估计和补偿。H.264的运动补偿支持以往的视频编码标准中的大部分关键特性,而且灵活地添加了更多的功能,除了支持P帧、B帧外,H.264还支持一种新的流间传送帧——SP帧,如图3所示。码流中包含SP帧后,能在有类似内容但有不同码率的码流之间快速切换,同时支持随机接入和快速回放模式。 - - - -### 3整数变换 - - - 在变换方面,H.264使用了基于4×4像素块的类似于DCT的变换,但使用的是以整数为基础的空间变换,不存在反变换,因为取舍而存在误差的问题,变换矩阵如图5所示。与浮点运算相比,整数DCT变换会引起一些额外的误差,但因为DCT变换后的量化也存在量化误差,与之相比,整数DCT变换引起的量化误差影响并不大。此外,整数DCT变换还具有减少运算量和复杂度,有利于向定点DSP移植的优点。 - - - -### 4量化 - - - H.264中可选32种不同的量化步长,这与H.263中有31个量化步长很相似,但是在H.264中,步长是以12.5%的复合率递进的,而不是一个固定常数。 - - 在H.264中,变换系数的读出方式也有两种:之字形(Zigzag)扫描和双扫描,如图6所示。大多数情况下使用简单的之字形扫描;双扫描仅用于使用较小量化级的块内,有助于提高编码效率。 - - - -### 5熵编码 - - - 视频编码处理的最后一步就是熵编码,在H.264中采用了两种不同的熵编码方法:通用可变长编码(UVLC)和基于文本的自适应二进制算术编码(CABAC)。 - - 在H.263等标准中,根据要编码的数据类型如变换系数、运动矢量等,采用不同的VLC码表。H.264中的UVLC码表提供了一个简单的方法,不管符号表述什么类型的数据,都使用统一变字长编码表。其优点是简单;缺点是单一的码表是从概率统计分布模型得出的,没有考虑编码符号间的相关性,在中高码率时效果不是很好。 - - 因此,H.264中还提供了可选的CABAC方法。算术编码使编码和解码两边都能使用所有句法元素(变换系数、运动矢量)的概率模型。为了提高算术编码的效率,通过内容建模的过程,使基本概率模型能适应随视频帧而改变的统计特性。内容建模提供了编码符号的条件概率估计,利用合适的内容模型,存在于符号间的相关性可以通过选择目前要编码符号邻近的已编码符号的相应概率模型来去除,不同的句法元素通常保持不同的模型。 - -## H.264在实时视频聊天中的应用 - - - 目前,H.264已被广泛应用于实时视频应用中,相比以往的方案使得在同等速率下,H.264能够比H.263减小50%的码率。也就是说,用户即使是只利用 384kbit/s的带宽,就可以享受H.263下高达 768kbit/s的高质量视频服务。H.264 不但有助于节省庞大开支,还可以提高资源的使用效率,同时令达到商业质量的实时视频服务拥有更多的潜在客户。 +TODO diff --git "a/VideoDevelopment/\350\247\206\351\242\221\347\274\226\347\240\201/H265.md" "b/VideoDevelopment/\350\247\206\351\242\221\347\274\226\347\240\201/H265.md" index 45aa49ec..418dae10 100644 --- "a/VideoDevelopment/\350\247\206\351\242\221\347\274\226\347\240\201/H265.md" +++ "b/VideoDevelopment/\350\247\206\351\242\221\347\274\226\347\240\201/H265.md" @@ -1,97 +1,9 @@ -H264 +H265 === - - -H.264是一种高性能的视频编解码技术。目前国际上制定视频编解码技术的组织有两个,一个是“国际电联”,它制定的标准有H.261、H.263、H.263+等,另一个是“国际标准化组织(ISO)”它制定的标准有MPEG-1、MPEG-2、MPEG-4等。而H.264则是由两个组织联合组建的联合视频组(JVT)共同制定的新数字视频编码标准,所以它既是ITU-T的H.264,又是ISO/IEC的MPEG-4高级视频编码,而且它将成为MPEG-4标准的第10部分。因此,不论是MPEG-4 AVC、MPEG-4 Part 10,还是ISO/IEC 14496-10,都是指H.264。 - - - - - -## H.264算法的优势 - - - H.264是在MPEG-4技术的基础之上建立起来的,其编解码流程主要包括5个部分:帧间和帧内预测、变换和反变换、量化和反量化、环路滤波、熵编码。 - - H.264/MPEG-4 AVC(H.264)是1995年自MPEG-2视频压缩标准发布以后的最新、最有前途的视频压缩标准。H.264是由ITU-T和ISO/IEC的联合开发组共同开发的最新国际视频编码标准。通过该标准,在同等图象质量下的压缩效率比以前的标准提高了2倍以上,因此,H.264被普遍认为是最有影响力的行业标准。 - -## H.264的优势 - - - H.264在1997年ITU的视频编码专家组提出时被称为H.26L,在ITU与ISO合作研究后被称为MPEG4 Part10或H.264(JVT)。H.264标准的主要目标是:与其它现有的视频编码标准相比,在相同的带宽下提供更加优秀的图象质量。 - -**而,H.264与以前的国际标准如H.263和MPEG-4相比,最大的优势体现在以下四个方面:** - - - -- 将每个视频帧分离成由像素组成的块,因此视频帧的编码处理的过程可以达到块的级别。 -- 采用空间冗余的方法,对视频帧的一些原始块进行空间预测、转换、优化和熵编码(可变长编码)。 -- 对连续帧的不同块采用临时存放的方法,这样,只需对连续帧中有改变的部分进行编码。该算法采用运动预测和运动补偿来完成。对某些特定的块,在一个或多个已经进行了编码的帧执行搜索来决定块的运动向量,并由此在后面的编码和解码中预测主块。 -- 采用剩余空间冗余技术,对视频帧里的残留块进行编码。例如:对于源块和相应预测块的不同,再次采用转换、优化和熵编码。 - - -**具体优势表现为:** - - - -- 低码流:和MPEG2和MPEG4 ASP等压缩技术相比,在同等图像质量下,采用H.264技术压缩后的数据量只有MPEG2的1/8,MPEG4的1/3。显然,H.264压缩技术的采用将大大节省用户的下载时间和数据流量收费。 -- 高质量的图象:H.264能提供连续、流畅的高质量图象(DVD质量)。 -- 容错能力强:H.264提供了解决在不稳定网络环境下容易发生的丢包等错误的必要工具。 -- 网络适应性强:H.264提供了网络适应层, 使得H.264的文件能容易地在不同网络上传输(例如互联网,CDMA,GPRS,WCDMA,CDMA2000等)。 - - - H.264和以前的标准一样,也是DPCM加变换编码的混合编码模式。但它采用“回归基本”的简洁设计,不用众多的选项,获得比H.263++好得多的压缩性能;加强了对各种信道的适应能力,采用“网络友好”的结构和语法,有利于对误码和丢包的处理;应用目标范围较宽,以满足不同速率、不同解析度以及不同传输(存储)场合的需求。 - -## H.264标准的关键技术 - - - -### 1 帧内预测编码 - - - 帧内编码用来缩减图像的空间冗余。为了提高H.264帧内编码的效率,在给定帧中充分利用相邻宏块的空间相关性,相邻的宏块通常含有相似的属性。因此,在对一给定宏块编码时,首先可以根据周围的宏块预测(典型的是根据左上角的宏块,因为此宏块已经被编码处理),然后对预测值与实际值的差值进行编码,这样,相对于直接对该帧编码而言,可以大大减小码率。 - - - -### 2帧间预测编码 - - - 帧间预测编码利用连续帧中的时间冗余来进行运动估计和补偿。H.264的运动补偿支持以往的视频编码标准中的大部分关键特性,而且灵活地添加了更多的功能,除了支持P帧、B帧外,H.264还支持一种新的流间传送帧——SP帧,如图3所示。码流中包含SP帧后,能在有类似内容但有不同码率的码流之间快速切换,同时支持随机接入和快速回放模式。 - - - -### 3整数变换 - - - 在变换方面,H.264使用了基于4×4像素块的类似于DCT的变换,但使用的是以整数为基础的空间变换,不存在反变换,因为取舍而存在误差的问题,变换矩阵如图5所示。与浮点运算相比,整数DCT变换会引起一些额外的误差,但因为DCT变换后的量化也存在量化误差,与之相比,整数DCT变换引起的量化误差影响并不大。此外,整数DCT变换还具有减少运算量和复杂度,有利于向定点DSP移植的优点。 - - - -### 4量化 - - - H.264中可选32种不同的量化步长,这与H.263中有31个量化步长很相似,但是在H.264中,步长是以12.5%的复合率递进的,而不是一个固定常数。 - - 在H.264中,变换系数的读出方式也有两种:之字形(Zigzag)扫描和双扫描,如图6所示。大多数情况下使用简单的之字形扫描;双扫描仅用于使用较小量化级的块内,有助于提高编码效率。 - - - -### 5熵编码 - - - 视频编码处理的最后一步就是熵编码,在H.264中采用了两种不同的熵编码方法:通用可变长编码(UVLC)和基于文本的自适应二进制算术编码(CABAC)。 - - 在H.263等标准中,根据要编码的数据类型如变换系数、运动矢量等,采用不同的VLC码表。H.264中的UVLC码表提供了一个简单的方法,不管符号表述什么类型的数据,都使用统一变字长编码表。其优点是简单;缺点是单一的码表是从概率统计分布模型得出的,没有考虑编码符号间的相关性,在中高码率时效果不是很好。 - - 因此,H.264中还提供了可选的CABAC方法。算术编码使编码和解码两边都能使用所有句法元素(变换系数、运动矢量)的概率模型。为了提高算术编码的效率,通过内容建模的过程,使基本概率模型能适应随视频帧而改变的统计特性。内容建模提供了编码符号的条件概率估计,利用合适的内容模型,存在于符号间的相关性可以通过选择目前要编码符号邻近的已编码符号的相应概率模型来去除,不同的句法元素通常保持不同的模型。 - -## H.264在实时视频聊天中的应用 - - - 目前,H.264已被广泛应用于实时视频应用中,相比以往的方案使得在同等速率下,H.264能够比H.263减小50%的码率。也就是说,用户即使是只利用 384kbit/s的带宽,就可以享受H.263下高达 768kbit/s的高质量视频服务。H.264 不但有助于节省庞大开支,还可以提高资源的使用效率,同时令达到商业质量的实时视频服务拥有更多的潜在客户。 +TODO From 39f8ca0d45a1fab5682b5b10f585573659c0b82d Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 7 Jul 2020 22:04:08 +0800 Subject: [PATCH 025/213] update --- JavaKnowledge/.DS_Store | Bin 6148 -> 0 bytes .../Git\347\256\200\344\273\213.md" | 15 +- ...37\347\220\206\345\210\206\346\236\220.md" | 684 +++++++++++++++++- 3 files changed, 667 insertions(+), 32 deletions(-) delete mode 100644 JavaKnowledge/.DS_Store diff --git a/JavaKnowledge/.DS_Store b/JavaKnowledge/.DS_Store deleted file mode 100644 index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0或HEAD]`:将当前的分支重设`(reset)`到指定的``或者`HEAD`(默认,如果不显示指定``,默认是`HEAD`,即最新的一次提交),并且根据`[mode]`有可能更新索引和工作目录。`mode`的取值可以是`hard、soft、mixed、merged、keep`。下面来详细说明每种模式的意义和效果: - - `--hard`:重设`(reset)`索引和工作目录,自从``以来在工作目录中的任何改变都被丢弃,并把`HEAD`指向``。会将其之后的修改全部撤回,并且会影响到工作区 - - `--mixed`改变分支和暂存区,不影响工作区 - - `soft`只改变分支的提交 + - `--hard`:彻底回退到某一个版本,本地的源码也会变为上一个版本的内容。重删除工作空间改动代码,撤销commit,撤销git add .。所有变更集都会被丢弃。 + - `--mixed`:默认方式,它回退到某个版本,只保留源码,不删除工作空间改动代码,撤销commit,并且撤销git add . 。所有变更集都放在工作区。 + - `--soft`: 回退到某个版本,不删除工作空间改动代码,撤销commit,不撤销git add . ,所有变更集都放在暂存区,如果还要提交直接重新commit即可。 下面是具体一个例子,假设有三个`commit`,执行`git status`结果如下: ``` @@ -262,11 +262,6 @@ git push // 把所有文件从本地仓库推送进远程仓库 但是上面这样会导致你之前修改的代码都没有了,如果我只是想撤回提交,还想要我之前修改的东西重新回到本地仓库呢? `git reset --soft HEAD^`,这样就成功的撤销了你的commit。注意,仅仅是撤回commit操作,您写的代码仍然保留。 - 至于这几个参数: - 1. --mixed:不删除工作空间改动代码,撤销commit,并且撤销git add . 操作这个为默认参数,git reset --mixed HEAD^ 和 git reset HEAD^ 效果是一样的。 - 2. --soft:不删除工作空间改动代码,撤销commit,不撤销git add . - 3. --hard:删除工作空间改动代码,撤销commit,撤销git add . 注意完成这个操作后,就恢复到了上一次的commit状态。 - - 已推送到远程仓库 如果你执行`git add .`后又`commit`又执行了`git push`操作了,这时候你的代码已经进入到了远程仓库中,如果你发现你提交的代码又问题想恢复的话,那你只能先把本地仓库的 代码恢复,然后再强制执行`git push`仓做,`push`到远程仓库就可以了。 @@ -367,7 +362,7 @@ git push // 把所有文件从本地仓库推送进远程仓库 在用`linux`的时候会自动生成一些以`~`结尾的备份文件,如果ignore掉呢?[https://github.com/github/gitignore/blob/master/Global/Linux.gitignore](https://github.com/github/gitignore/blob/master/Global/Linux.gitignore) - 撤销最后一次提交 - 有时候我们提交完了才发现漏掉了几个文件没有加或者提交信息写错了,想要撤销刚才的的提交操作。可以使用`--amend`选项重新提交:`git commit --amend`,然后再执行`git push`操作。 + 有时候我们提交完了才发现漏掉了几个文件没有加或者提交信息写错了,想要撤销刚才的的提交操作。可以修改后重新git add 然后使用`--amend`选项重新提交:`git commit --amend`,然后再执行`git push`操作。 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 d1808e42..63b0d41f 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,12 +1,31 @@ HashMap实现原理分析 === -`HashMap`主要是用数组来存储数据的,我们都知道它会对`key`进行哈希运算,哈系运算会有重复的哈希值,对于哈希值的冲突,`HashMap`采用链表来解决的。 -`在HashMap`里有这样的一句属性声明: +HashMap基于Map接口实现,元素以键值对的方式存储,并且允许使用null建和null值,因为key不允许重复,因此只能有一个键为null,另外HashMap不能保证放入元素的顺序,它是无序的,和放入的顺序并不能相同。 + + + +## 原理 + +其底层数据结构是数组称之为哈希桶,每个桶(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时,顺序是按照哈希桶从低到高,链表从前往后,依次遍历的。 + + + + + +## JDK1.7 + +HashMap在JDK1.8中发生了改变,下面的部分是基于JDK1.7的分析。HashMap主要是用数组来存储数据的,我们都知道它会对key进行哈希运算,哈希运算会有重复的哈希值,对于哈希值的冲突,HashMap采用链表来解决的。 +在HashMap里有这样的一句属性声明: + ```java transient Entry[] table; ``` -可以看到`Map`是通过数组的方式来储存`Entry`那`Entry`是神马呢?就是`HashMap`存储数据所用的类,它拥有的属性如下: +可以看到Map是通过数组的方式来储存Entry那Entry是神马呢?就是HashMap存储数据所用的类,它拥有的属性如下: ```java static class Entry implements Map.Entry { final K key; @@ -16,20 +35,24 @@ static class Entry implements Map.Entry { ...//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`类的对象。`HashMap`类有一个叫做`Entry`的内部类。这个`Entry`类包含了`key-value`作为实例变量。 -- 每当往`Hashmap`里面存放`key-value`对的时候,都会为它们实例化一个`Entry`对象,这个`Entry`对象就会存储在前面提到的`Entry`数组`table`中。 -现在你一定很想知道,上面创建的`Entry`对象将会存放在具体哪个位置(在`table`中的精确位置)。答案就是,根据`key`的`hashcode()`方法计算出来的`hash`值来决定。 -`hash`值用来计算`key`在`Entry`数组的索引。 -- 我们往`hashmap`放了4个`key-value`对,但是有时候看上去好像只有2个元素!!!这是因为,如果两个元素有相同的`hashcode`,它们会被放在同一个索引上。 -问题出现了,该怎么放呢?原来它是以链表`(LinkedList)`的形式来存储的。 +- HashMap中有一个叫table的Entry数组。这个数组存储了Entry类的对象。Entry类包含了key-value作为实例变量。 + +- table的索引在逻辑上叫做“桶”(bucket),它存储了链表的第一个元素。 + +- 每当往Hashmap里面存放key-value对的时候,都会为它们实例化一个Entry对象,这个Entry对象就会存储在前面提到的Entry数组table中。根据key的hashcode()方法计算出来的hash值来决定在Entry数组的索引(所在的桶)。 + +- 如果两个key有相同的hash值,他们会被放在table数组的同一个桶里面。 + +- key的equals()方法用来确保key的唯一性。 + + + +接下来看一下put方法: -接下来看一下`put`方法: ```java /** * Associates the specified value with the specified key in this map. If the @@ -73,7 +96,7 @@ public V put(K key, V value) { return null; } ``` -再看一下`get`方法:     +再看一下get方法:     ```java /** * Returns the value to which the specified key is mapped, or {@code null} @@ -110,15 +133,632 @@ public V get(Object key) { } ``` -总结: -- `HashMap`有一个叫做`Entry`的内部类,它用来存储`key-value`对。 -- 上面的`Entry`对象是存储在一个叫做`table`的`Entry`数组中。 -- `table`的索引在逻辑上叫做“桶”`(bucket)`,它存储了链表的第一个元素。 -- `key`的`hashcode()`方法用来找到`Entry`对象所在的桶。 -- 如果两个`key`有相同的`hash`值,他们会被放在`table`数组的同一个桶里面。 -- `key`的`equals()`方法用来确保`key`的唯一性。 -- `value`对象的`equals()`和`hashcode()`方法根本一点用也没有。 + +## JDK1.8 + +在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类: + + ```java + // 单链表 + static class Node implements Map.Entry { + // 用于定位数组的索引位置 + final int hash; + final K key; + V value; + // 链表的下一个node + Node next; + + Node(int hash, K key, V value, Node next) { + this.hash = hash; + this.key = key; + this.value = value; + this.next = next; + } + + public final K getKey() { return key; } + public final V getValue() { return value; } + public final String toString() { return key + "=" + value; } + // 每一个节点的hashcode值,是将key和value的hashCode值异或得到的 + public final int hashCode() { + return Objects.hashCode(key) ^ Objects.hashCode(value); + } + + public final V setValue(V newValue) { + V oldValue = value; + value = newValue; + return oldValue; + } + + public final boolean equals(Object o) { + if (o == this) + return true; + if (o instanceof Map.Entry) { + Map.Entry e = (Map.Entry)o; + if (Objects.equals(key, e.getKey()) && + Objects.equals(value, e.getValue())) + return true; + } + return false; + } + } + ``` + + ```java + static class LinkedHashMapEntry extends HashMap.Node { + LinkedHashMapEntry before, after; + LinkedHashMapEntry(int hash, K key, V value, Node next) { + super(hash, key, value, next); + } + } + ``` + + ```java + // 红黑树 + static final class TreeNode extends LinkedHashMap.LinkedHashMapEntry { + TreeNode parent; // red-black tree links + TreeNode left; + TreeNode right; + TreeNode prev; // needed to unlink next upon deletion + boolean red; + TreeNode(int hash, K key, V val, Node next) { + super(hash, key, val, next); + } + /** + * Forms tree of the nodes linked from this node. + * @return root of tree + */ + final void treeify(Node[] tab) { + ... + } + + /** + * Returns a list of non-TreeNodes replacing those linked from + * this node. + */ + final Node untreeify(HashMap map) { + ... + } + + /** + * 红黑树的插入 + * Tree version of putVal. + */ + final TreeNode putTreeVal(HashMap map, Node[] tab, + int h, K k, V v) { + Class kc = null; + boolean searched = false; + TreeNode root = (parent != null) ? root() : this; + for (TreeNode p = root;;) { + int dir, ph; K pk; + if ((ph = p.hash) > h) + dir = -1; + else if (ph < h) + dir = 1; + else if ((pk = p.key) == k || (k != null && k.equals(pk))) + return p; + else if ((kc == null && + (kc = comparableClassFor(k)) == null) || + (dir = compareComparables(kc, k, pk)) == 0) { + if (!searched) { + TreeNode q, ch; + searched = true; + if (((ch = p.left) != null && + (q = ch.find(h, k, kc)) != null) || + ((ch = p.right) != null && + (q = ch.find(h, k, kc)) != null)) + return q; + } + dir = tieBreakOrder(k, pk); + } + + TreeNode xp = p; + if ((p = (dir <= 0) ? p.left : p.right) == null) { + Node xpn = xp.next; + TreeNode x = map.newTreeNode(h, k, v, xpn); + if (dir <= 0) + xp.left = x; + else + xp.right = x; + xp.next = x; + x.parent = x.prev = xp; + if (xpn != null) + ((TreeNode)xpn).prev = x; + moveRootToFront(tab, balanceInsertion(root, x)); + return null; + } + } + } + } + ``` + +2. 再来看一下HashMap的实现类及构造函数 + + ```java + public class HashMap extends AbstractMap + implements Map, Cloneable, Serializable { + // 默认的初始化容量大小,必须是2的倍数,默认是16,为啥必须是2的倍数,后面会说 + static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 + // 在构造函数中未指定时使用的负载因子。 + static final float DEFAULT_LOAD_FACTOR = 0.75f; + // 使用红黑树树而不链表的容器计数阈值 + static final int TREEIFY_THRESHOLD = 8; + // 用于在执行过程中取消使用红黑树(拆分)箱调整大小操作的箱计数阈值, + static final int UNTREEIFY_THRESHOLD = 6; + // 哈希桶,存储数据的数组 + transient Node[] table; + // Node[] table的初始化长度length(默认值是16),Load factor为负载因子(默认值是0.75),threshold是HashMap所能容纳的最大数据量的Node(键值对)个数。threshold = (capacity * load factor),超过这个数量后就会进行重新resize(扩容),扩容一后的HashMap容量是之前容量的两倍 + int threshold; + + + public HashMap(int initialCapacity, float loadFactor) { + if (initialCapacity < 0) + throw new IllegalArgumentException("Illegal initial capacity: " + + initialCapacity); + if (initialCapacity > MAXIMUM_CAPACITY) + initialCapacity = MAXIMUM_CAPACITY; + if (loadFactor <= 0 || Float.isNaN(loadFactor)) + throw new IllegalArgumentException("Illegal load factor: " + + loadFactor); + this.loadFactor = loadFactor; + this.threshold = tableSizeFor(initialCapacity); + } + + public HashMap(int initialCapacity) { + this(initialCapacity, DEFAULT_LOAD_FACTOR); + } + + /** + * Constructs an empty HashMap with the default initial capacity + * (16) and the default load factor (0.75). + */ + public HashMap() { + this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted + } + } + ``` + +### 确定哈希桶数组索引位置 + +不管增加、删除、查找键值对,定位到哈希桶数组的位置都是很关键的第一步。前面说过HashMap的数据结构是数组和链表的结合,所以我们当然希望这个HashMap里面的元素位置尽量分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,不用遍历链表,大大优化了查询的效率。HashMap定位数组索引位置,直接决定了hash方法的离散性能。先看看源码的实现: + +```java +static final int hash(Object key) { + int h; + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); +} +``` + +对于任意给定的对象,只要它的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) 将是一个非常巧妙的设计: + +- 假设h=5,length=16, 那么h & length - 1将得到 5; +- 如果h=6,length=16, 那么h & length - 1将得到 6 +- 如果h=15,length=16, 那么h & length - 1将得到 15; +- 但是当h=16时 , length=16 时,那么h & length - 1将得到0了; +- 当h=17时, length=16时,那么h & length - 1将得到1了。 + +这样保证计算得到的索引值总是位于 table 数组的索引之内。 + + + +在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 + public V put(K key, V value) { + // 对key调用hash()方法获取hash值 + return putVal(hash(key), key, value, false, true); + } + + + final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { + Node[] tab; Node p; int n, i; + // 1. 如果table还没初始化就初始化 + if ((tab = table) == null || (n = tab.length) == 0) + n = (tab = resize()).length; + // 2. 根据键值key计算的hash值来得到插入的数组索引i,判断tab[i]是否为null,数组索引通过(n -1) & hash来获得 + if ((p = tab[i = (n - 1) & hash]) == null) + // 3. 如果tab[索引]的值为null,那就说明这个索引没有数组桶,直接新建一个数组桶 + tab[i] = newNode(hash, key, value, null); + else { + // 4. 否则就是目前已经存在该索引的数组桶了,要继续判断是链表还是红黑树。 + Node e; K k; + // 5. 判断table[索引]处的收个元素是否和key一样,如果首个就是那就直接覆盖value,不用再找了 + if (p.hash == hash && + ((k = p.key) == key || (key != null && key.equals(k)))) + // 6. 数组桶的首个元素就是,直接覆盖value的值 + e = p; + else if (p instanceof TreeNode) + // 7. 数组桶首个元素不是,并且链表是红黑树,红黑树直接插入键值对 + e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); + else { + // 8. 目前为链表,开始遍历链表准备插入 + for (int binCount = 0; ; ++binCount) { + if ((e = p.next) == null) { + // 添加到目前的节点的next上 + p.next = newNode(hash, key, value, null); + if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st + // 判断链表长度是否大于8,如果大于直接转换成红黑树,插入键值对 + treeifyBin(tab, hash); + break; + } + + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + break; + p = e; + } + } + if (e != null) { // existing mapping for key + V oldValue = e.value; + if (!onlyIfAbsent || oldValue == null) + e.value = value; + afterNodeAccess(e); + return oldValue; + } + } + ++modCount; + // 判断当前存在的键值对数量size是否超过了最大容量threshold,如果超过就调用resize扩容 + if (++size > threshold) + resize(); + afterNodeInsertion(evict); + return null; + } +``` + +上面主要有两个方法一个是红黑树的插入,一个是treeifyBin()也就是把链表转换成红黑树 + +```java +// MIN_TREEIFY_CAPACITY 的值为64,若当前table的length不够,则resize() +// 将桶内所有的 链表节点 替换成 红黑树节点 +final void treeifyBin(Node[] tab, int hash) { + int n, index; Node e; + // 如果当前哈希表为空,或者哈希表中元素的个数小于树形化阈值(默认为 64),就去新建(扩容) + if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) + resize(); + // 如果哈希表中的元素个数超过了树形化阈值,则进行树形化 + // e 是哈希表中指定位置桶里的链表节点,从第一个开始 + else if ((e = tab[index = (n - 1) & hash]) != null) { + // 红黑树的头、尾节点 + TreeNode hd = null, tl = null; + do { + // 新建一个树形节点,内容和当前链表节点 e 一致 + TreeNode p = replacementTreeNode(e, null); + // 确定树头节点 + if (tl == null) + hd = p; + else { + p.prev = tl; + tl.next = p; + } + tl = p; + } while ((e = e.next) != null); + // 让桶的第一个元素指向新建的红黑树头结点,以后这个桶里的元素就是红黑树而不是链表了 + if ((tab[index] = hd) != null) + hd.treeify(tab); + } +} +``` + +### resize()扩容 + +扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组,就像我们用一个小桶装水,如果想装更多的水,就得换大水桶。 + +resize()方法用于初始化数组或数组扩容,每次扩容后容量为原来的2倍,并进行数据迁移。 + + + +例如我们从 16 扩展为 32 时,具体的变化如下所示: + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/resize1.bmp) + + + + +因此元素在重新计算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示意图: + +![resize](https://raw.githubusercontent.com/CharonChui/Pictures/master/resize3.bmp) + + + + +这个设计确实非常的巧妙,既省去了重新计算 hash 值的时间,而且同时,**由于新增的 1bit 是 0 还是 1 可以认为是随机的,因此 resize 的过程,均匀的把之前的冲突的节点分散到新的 bucket 了**。 + +```java +final Node[] resize() { + //oldTab 为当前表的哈希桶 + Node[] oldTab = table; + //当前哈希桶的容量 length + int oldCap = (oldTab == null) ? 0 : oldTab.length + //当前的阈值 + int oldThr = threshold; + //初始化新的容量和阈值为 0 + int newCap, newThr = 0; + //如果当前容量大于 0 + if (oldCap > 0) { + //超过最大值就不再扩充了,就只好随你碰撞去吧 + if (oldCap >= MAXIMUM_CAPACITY) { + threshold = Integer.MAX_VALUE; + return oldTab; + } + //没超过最大值,就扩充为原来的 2 倍 + else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && + oldCap >= DEFAULT_INITIAL_CAPACITY) + //如果旧的容量大于等于默认初始容量 16, 那么新的阈值也等于旧的阈值的两倍 + newThr = oldThr << 1; // double threshold + } + else if (oldThr > 0) // initial capacity was placed in threshold + newCap = oldThr;//那么新表的容量就等于旧的阈值 + else {// zero initial threshold signifies using defaults + newCap = DEFAULT_INITIAL_CAPACITY;//此时新表的容量为默认的容量 16 + newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//新的阈值为默认容量 16 * 默认加载因子 0.75f = 12 + } + if (newThr == 0) {//如果新的阈值是 0,对应的是当前表是空的,但是有阈值的情况 + float ft = (float)newCap * loadFactor;//根据新表容量和加载因子求出新的阈值 + //进行越界修复 + newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); + } + //更新阈值 + threshold = newThr; + @SuppressWarnings({"rawtypes","unchecked"}) + //根据新的容量构建新的哈希桶 + Node[] newTab = (Node[])new Node[newCap]; + //更新哈希桶引用 + table = newTab; + //如果以前的哈希桶中有元素 + //下面开始将当前哈希桶中的所有节点转移到新的哈希桶中 + if (oldTab != null) { + //把每个 bucket 都移动到新的 buckets 中 + for (int j = 0; j < oldCap; ++j) { + //取出当前的节点 e + Node e; + //如果当前桶中有元素,则将链表赋值给 e + if ((e = oldTab[j]) != null) { + //将原哈希桶置空以便 GC + oldTab[j] = null; + //如果当前链表中就一个元素,(没有发生哈希碰撞) + if (e.next == null) + //直接将这个元素放置在新的哈希桶里。 + //注意这里取下标是用哈希值与桶的长度-1。由于桶的长度是2的n次方,这么做其实是等于一个模运算。但是效率更高 + newTab[e.hash & (newCap - 1)] = e; + //如果发生过哈希碰撞 ,而且是节点数超过8个,转化成了红黑树 + else if (e instanceof TreeNode) + ((TreeNode)e).split(this, newTab, j, oldCap); + //如果发生过哈希碰撞,节点数小于 8 个。则要根据链表上每个节点的哈希值,依次放入新哈希桶对应下标位置。 + else { // preserve order + //因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即 low 位, 或者扩容后的下标,即 high 位。 high 位 = low 位 + 原哈希桶容量 + //低位链表的头结点、尾节点 + Node loHead = null, loTail = null; + //高位链表的头节点、尾节点 + Node hiHead = null, hiTail = null; + Node next;//临时节点 存放 e 的下一个节点 + do { + next = e.next; + //这里又是一个利用位运算 代替常规运算的高效点:利用哈希值与旧的容量,可以得到哈希值去模后,是大于等于 oldCap 还是小于 oldCap,等于 0 代表小于 oldCap,应该存放在低位,否则存放在高位 + if ((e.hash & oldCap) == 0) { + //给头尾节点指针赋值 + if (loTail == null) + loHead = e; + else + loTail.next = e; + loTail = e; + }//高位也是相同的逻辑 + else { + if (hiTail == null) + hiHead = e; + else + hiTail.next = e; + hiTail = e; + }//循环直到链表结束 + } while ((e = next) != null); + //将低位链表存放在原 index 处, + if (loTail != null) { + loTail.next = null; + newTab[j] = loHead; + } + //将高位链表存放在新 index 处 + if (hiTail != null) { + hiTail.next = null; + newTab[j + oldCap] = hiHead; + } + } + } + } + } + return newTab; +} +``` + +再看一下往哈希表里插入一个节点的putVal函数,如果参数onlyIfAbsent是true,那么不会覆盖相同key的值value。如果evict是false。那么表示是在初始化时调用的 + +```java +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { + //tab存放 当前的哈希桶, p用作临时链表节点 + Node[] tab; Node p; int n, i; + //如果当前哈希表是空的,代表是初始化 + if ((tab = table) == null || (n = tab.length) == 0) + //那么直接去扩容哈希表,并且将扩容后的哈希桶长度赋值给n + n = (tab = resize()).length; + //如果当前index的节点是空的,表示没有发生哈希碰撞。 直接构建一个新节点Node,挂载在index处即可。 + //这里再啰嗦一下,index 是利用 哈希值 & 哈希桶的长度-1,替代模运算 + if ((p = tab[i = (n - 1) & hash]) == null) + tab[i] = newNode(hash, key, value, null); + else {//否则 发生了哈希冲突。 + //e + Node e; K k; + //如果哈希值相等,key也相等,则是覆盖value操作 + if (p.hash == hash && + ((k = p.key) == key || (key != null && key.equals(k)))) + e = p;//将当前节点引用赋值给e + else if (p instanceof TreeNode)//红黑树暂且不谈 + e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); + else {//不是覆盖操作,则插入一个普通链表节点 + //遍历链表 + for (int binCount = 0; ; ++binCount) { + if ((e = p.next) == null) {//遍历到尾部,追加新节点到尾部 + p.next = newNode(hash, key, value, null); + //如果追加节点后,链表数量》=8,则转化为红黑树 + if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st + treeifyBin(tab, hash); + break; + } + //如果找到了要覆盖的节点 + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + break; + p = e; + } + } + //如果e不是null,说明有需要覆盖的节点, + if (e != null) { // existing mapping for key + //则覆盖节点值,并返回原oldValue + V oldValue = e.value; + if (!onlyIfAbsent || oldValue == null) + e.value = value; + //这是一个空实现的函数,用作LinkedHashMap重写使用。 + afterNodeAccess(e); + return oldValue; + } + } + //如果执行到了这里,说明插入了一个新的节点,所以会修改modCount,以及返回null。 + + //修改modCount + ++modCount; + //更新size,并判断是否需要扩容。 + if (++size > threshold) + resize(); + //这是一个空实现的函数,用作LinkedHashMap重写使用。 + afterNodeInsertion(evict); + return null; +} +``` + + + +扩容就是使用一个容量更大的数组来代替已有的容量小的数组,transfer()方法将原有Entry数组的元素拷贝到新的Entry数组里。 + +* 运算尽量都用位运算代替,更高效。 +* 对于扩容导致需要新建数组存放更多元素时,除了要将老数组中的元素迁移过来,也记得将老数组中的引用置null,以便GC +* 取下标 是用 哈希值 与运算 (桶的长度-1) i = (n - 1) & hash。 由于桶的长度是2的n次方,这么做其实是等于 一个模运算。但是效率更高 +* 扩容时,如果发生过哈希碰撞,节点数小于8个。则要根据链表上每个节点的哈希值,依次放入新哈希桶对应下标位置。 +* 因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位, 或者扩容后的下标,即high位。 high位= low位+原哈希桶容量 +* 利用哈希值 与运算 旧的容量 ,if ((e.hash & oldCap) == 0),可以得到哈希值去模后,是大于等于oldCap还是小于oldCap,等于0代表小于oldCap,应该存放在低位,否则存放在高位。这里又是一个利用位运算 代替常规运算的高效点 +* 如果追加节点后,链表数量》=8,则转化为红黑树 +* 插入节点操作时,有一些空实现的函数,用作LinkedHashMap重写使用。 + + +### get + +```java +public V get(Object key) { + Node e; + return (e = getNode(hash(key), key)) == null ? null : e.value; +} + +final Node getNode(int hash, Object key) { + Node[] tab; Node first, e; int n; K k; + if ((tab = table) != null && (n = tab.length) > 0 && + (first = tab[(n - 1) & hash]) != null) { + // 找到索引,判断数组桶中的第一个元素是不是 + if (first.hash == hash && // always check first node + ((k = first.key) == key || (key != null && key.equals(k)))) + return first; + if ((e = first.next) != null) { + // 第一个元素不是的话,就看是不是红黑树还是链表,然后一直去找 + if (first instanceof TreeNode) + return ((TreeNode)first).getTreeNode(hash, key); + do { + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + return e; + } while ((e = e.next) != null); + } + } + return null; +} +``` + + + + + +## JDK 7 与 JDK 8 中关于 HashMap的对比 + +1. JDK8为红黑树 + 链表 + 数组的形式,当桶内元素大于8时,便会树化。 +2. hash值的计算方式不同 (jdk 8 简化)。 +3. JDK7中table在创建hashmap时分配空间,而8中在put的时候分配。 +4. 在发生冲突,插入链中时,7 是头插法,8 是尾插法 +5. 在resize操作中,7 需要重新进行index的计算,而8不需要,通过判断相应的位是0还是1,要么依旧是原index,要么是oldCap + 原 index。 + + + + + +## 问题 + +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最后四位。这种算法在保持分布均匀之外,效率也非常高。 + + + +2. 为什么需要使用加载因子,为什么需要扩容呢? + + 因为如果填充比很大,说明利用的空间很多,如果一直不进行扩容的话,链表就会越来越长,这样查找的效率很低,因为链表的长度很大(当然最新版本使用了红黑树后会改进很多),扩容之后,将原来链表数组的每一个链表分成奇偶两个子链表分别挂在新链表数组的散列位置,这样就减少了每个链表的长度,增加查找效率。HashMap 本来是以空间换时间,所以填充比没必要太大。但是填充比太小又会导致空间浪费。如果关注内存,填充比可以稍大,如果主要关注查找性能,填充比可以稍小。 + +3. 为什么 HashMap 是线程不安全的,实际会如何体现? + + - 如果多个线程同时使用put方法添加元素,假设正好存在两个put的key发生了碰撞 (hash值一样),那么根据HashMap的实现,这两个key会添加到数组的同一个位置,这样最终就会发生其中一个线程的put的数据被覆盖。 + + - 如果多个线程同时检测到元素个数超过数组大小 * loadFactor。这样会发生多个线程同时对hash数组进行扩容,都在重新计算元素位置以及复制数据,但是最终只有一个线程扩容后的数组会赋给table,也就是说其他线程的都会丢失,并且各自线程put的数据也丢失。且会引起死循环的错误。 + + + +4. 与HashTable的区别 + + Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,它的并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。 + + - 与之相比HashTable是线程安全的,且不允许key、value是null。 + - HashTable默认容量是11。 + - HashTable是直接使用key的hashCode(key.hashCode())作为hash值,不像HashMap内部使用static final int hash(Object key)扰动函数对key的hashCode进行扰动后作为hash值。 + - HashTable取哈希桶下标是直接用模运算%.(因为其默认容量也不是2的n次方。所以也无法用位运算替代模运算) + - 扩容时,新容量是原来的2倍+1。int newCapacity = (oldCapacity << 1) + 1; + - Hashtable是Dictionary的子类同时也实现了Map接口,HashMap是Map接口的一个实现类 + + + + +参考: + +- [从 JDK7 与 JDK8 对比详细分析 HashMap 的原理与优化](https://allenmistake.top/2019/05/13/hashmap/) +- [面试必备:HashMap源码解析(JDK8)](https://blog.csdn.net/zxt0601/article/details/77413921) + + + + + + --- - 邮箱 :charon.chui@gmail.com From 00746b815f9d33559f697306e43499f9a97571cb Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 9 Jul 2020 17:51:36 +0800 Subject: [PATCH 026/213] add notes --- ...ps\347\232\204\345\214\272\345\210\253.md" | 26 +- ...36\346\224\266\346\234\272\345\210\266.md" | 473 +++++++++++++++--- ...14Synchronized\345\214\272\345\210\253.md" | 303 ++++++++++- ...06\345\217\212\350\247\243\345\257\206.md" | 18 +- 4 files changed, 715 insertions(+), 105 deletions(-) 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 3c23a227..90346bfb 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" @@ -1,8 +1,21 @@ -Http与Https的区别 +HTTP与HTTPS的区别 === -`http`(超文本传输协议) ---- +## HTTP(超文本传输协议) + +超文本传输协议,是一个基于请求与响应,无状态的,应用层的协议,常基于TCP/IP协议传输数据,互联网上应用最为广泛的一种网络协议,所有的WWW文件都必须遵守这个标准。设计HTTP的初衷是为了提供一种发布和接收HTML页面的方法。 + + + +- 1997年发布HTTP/1.1: 持久连接(长连接)、节约带宽、HOST域、管道机制、分块传输编码。 + +- 2015年发布HTTP/2:多路复用、服务器推送、头信息压缩、二进制协议等。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/http1.1vs2.jpg) + +多路复用:通过单一的HTTP/2连接请求发起多重的请求-响应消息,多个请求stream共享一个TCP连接,实现多留并行而不是依赖建立多个TCP连接。 + + 缺点: @@ -14,11 +27,9 @@ Http与Https的区别 - 传输速度快 +## HTTPS -`https` ---- - -`Https`并非是应用层的一种新协议。只是`http`通信接口部分用`SSL`(安全套接字层)和`TLS`(安全传输层协议)代替而已。即添加了加密及认证机制的`HTTP`称为`HTTPS(HTTP Secure)`. +Https并非是应用层的一种新协议。只是http通信接口部分用SSL(安全套接字层)和TLS(安全传输层协议)代替而已。即添加了加密及认证机制的HTTP称为HTTPS(HTTP Secure). ``` HTTP + 加密 + 认证 + 完整性保护 = HTTPS @@ -136,7 +147,6 @@ HTTP + 加密 + 认证 + 完整性保护 = HTTPS - ---- - 邮箱 :charon.chui@gmail.com - Good Luck! 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 03e7f1cd..1b1dfb51 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,8 +1,79 @@ -JVM垃圾回收机制 -=== +# JVM垃圾回收机制 -引用计数算法 ---- + + +## JVM内存模式 + +JVM内存模型可以分为两个部分,如下图所示,**堆和方法区是所有线程共有的**,而虚拟机栈,本地方法栈和程序计数器则是线程私有的。 + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/jvm.png) + +### 堆(Heap) + +Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap)。Java的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时,Java 自动内存管理最核心的功能是堆内存中对象的分配与回收。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。 + +**堆空间的基本结构:** + +![](http://raw.githubusercontent.com/CharonChui/Pictures/master/java_heap.png) + +在我们垃圾回收的时候,我们往往将堆内存分成**新生代和老生代(大小比例1:2)**,新生代中由Eden和Survivor0,Survivor1组成,**三者的比例是8:1:1**,新生代的回收机制采用**复制算法**,大部分情况,对象都会首先在Eden区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入s0或者s1,并且对象的年龄还会加 1(Eden区->Survivor区后对象的初始年龄变为1),当它的年龄增加到一定程度(默认为15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。老生代采用的回收算法是**标记整理算法。** + + + +#### 对象优先在eden区分配 + +目前主流的垃圾收集器都会采用分代回收算法,因此需要将堆内存分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。 + +大多数情况下,对象在新生代中eden区分配。当eden区没有足够空间进行分配时,虚拟机将发起一次Minor 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 倍以上。 + +#### 大对象直接进入老年代 + +大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。这样做主要是为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。 + +#### 长期存活的对象将进入老年代 + +既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。 + +如果对象在Eden出生并经过第一次Minor GC后仍然能够存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1.对象在Survivor中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(一般都会说默认为 15 岁,其实默认晋升年龄并不都是15,这个是要区分垃圾收集器的,CMS就是6.),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。 + + + +### 方法区(Method Area) + +方法区也称”永久代“,它用于存储虚拟机加载的类信息、常量、静态变量、是各个线程共享的内存区域。默认最小值为16MB,最大值为64MB(64位JVM由于指针膨胀,默认是85M),可以通过-XX:PermSize 和 -XX:MaxPermSize 参数限制方法区的大小。它是一片连续的堆空间,永久代的垃圾收集是和老年代(old generation)捆绑在一起的,因此无论谁满了,都会触发永久代和老年代的垃圾收集。不过,一个明显的问题是,当JVM加载的类信息容量超过了参数-XX:MaxPermSize设定的值时,应用将会报OOM的错误。参数是通过-XX:PermSize和-XX:MaxPermSize来设定的。 + + + +### 虚拟机栈(JVM Stack) + +描述的是java方法执行的内存模型:每个方法被执行的时候都会创建一个”栈帧”,用于存储局部变量表(包括参数)、操作栈、方法出口等信息。每个方法被调用到执行完的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程**。**声明周期与线程相同,是线程私有的。栈帧由三部分组成:局部变量区、操作数栈、帧数据区。局部变量区被组织为以一个字长为单位、从0开始计数的数组,和局部变量区一样,操作数栈也被组织成一个以字长为单位的数组。但和前者不同的是,它不是通过索引来访问的,而是通过入栈和出栈来访问的,可以看作为临时数据的存储区域。除了局部变量区和操作数栈外,java栈帧还需要一些数据来支持常量池解析、正常方法返回以及异常派发机制。这些数据都保存在java栈帧的帧数据区中。 + +局部变量表: 存放了编译器可知的各种基本数据类型、对象引用(引用指针,并非对象本身),其中64位长度的long和double类型的数据会占用2个局部变量的空间,其余数据类型只占1个。局部变量表所需的内存空间**在**编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量是完全确定的,在运行期间栈帧不会改变局部变量表的大小空间。 + + + +### 本地方法栈(Native Stack) + +与虚拟机栈基本类似,区别在于虚拟机栈为虚拟机执行的java方法服务,而本地方法栈则是为Native方法服务(栈的空间大小远远小于堆)。 + +### 程序计数器(PC Register) + +最小的一块内存区域,它的作用是当前线程所执行的字节码的行号指示器,在虚拟机的模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令**,**分支**、**循环、异常处理、线程恢复等基础功能都需要依赖计数器完成。 + +### 直接内存 + +直接内存并不是虚拟机内存的一部分,也不是Java虚拟机规范中定义的内存区域。jdk1.4中新加入的NIO,引入了通道与缓冲区的IO方式,它可以调用Native方法直接分配堆外内存,这个堆外内存就是本机内存,不会影响到堆内存的大小. + + + +## 如何判断对象是垃圾 + +### 引用计数算法 在`JDK1.2`之前,使用的是引用计数器算法,即当这个类被加载到内存以后,就会产生方法区, 堆栈、程序计数器等一系列信息,当创建对象的时候,为这个对象在堆栈空间中分配对象, @@ -10,6 +81,7 @@ JVM垃圾回收机制 而当其中一个引用销毁的时候,引用计数器-1,当引用计数器被减为零的时候, 标志着这个对象已经没有引用了,可以回收了! 这种算法在JDK1.2之前的版本被广泛使用,但是随着业务的发展,很快出现了一个问题,那就是互相引用的问题: + ```java ObjA.obj = ObjB ObjB.obj - ObjA @@ -20,8 +92,11 @@ ObjB.obj - ObjA ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/yinyongjishu.jpg) -根搜索算法 ---- + + + + +### 根搜索算法 根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图, 从一个节点`GC ROOT`开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点, @@ -46,69 +121,335 @@ ObjB.obj - ObjA 那我们就继续分析下这三种算法: -- 标记-清除算法(Mark-Sweep) - ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/biaoji_qingchu.jpg) - 标记-清除算法采用从根集合进行扫描,对存活的对象对象标记,标记完毕后,再扫描整个空间中未被标记 的对象,进行回收,如上图所示。 - 标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片! - -- 复制算法(Copying) - ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/fuzhisuanfa.jpg) - - 复制算法采用从根集合扫描,并将存活对象复制到一块新的,没有使用过的空间中,这种算法当控件存活的对象比较少时,极为高效,但是带来的成本是需要一块内存交换空间用于进行对象的移动。 - -- 标记-整理算法(Mark-Compact) - ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/biaoji_zhengli.jpg) - - 整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。 - -- 分代收集算法(Generational Collection) - 分代收集算法是目前大部分`JVM`的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。 - 一般情况下将堆区划分为老年代(`Tenured Generation`)和新生代(`Young Generation`),老年代的特点是每次垃圾收集时只有少量对象需要被回收, - 而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。 -  目前大部分垃圾收集器对于新生代都采取复制算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少, - 但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的`Eden`空间和两块较小的`Survivor`空间, - 每次使用`Eden`空间和其中的一块`Survivor`空间,当进行回收时,将`Eden`和`Survivor`中还存活的对象复制到另一块`Survivor`空间中, - 然后清理掉`Eden`和刚才使用过的`Survivor`空间。 - -  而由于老年代的特点是每次回收都只回收少量对象,一般使用的是标记-整理算法。 -  注意,在堆区之外还有一个代就是永久代(`Permanet Generation`),它用来存储`class`类、常量、方法描述等。对永久代的回收主要回收两部分内容: - 废弃常量和无用的类。 - ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/xinshengdai.jpg) - 对象的内存分配,往大方向上讲就是在堆上分配,对象主要分配在新生代的`Eden Space`和`From Space`, - 少数情况下会直接分配在老年代。如果新生代的`Eden Space`和`From Space`的空间不足,则会发起一次`GC`,如果进行了`GC`之后,`Eden Space`和`From Space` - 能够容纳该对象就放在`Eden Space`和`From Space`。在`GC`的过程中,会将`Eden Space`和`From Space`中的存活对象移动到`To Space`, - 然后将`Eden Space`和`From Space`进行清理。如果在清理的过程中,`To Space`无法足够来存储某个对象,就会将该对象移动到老年代中。 - 在进行了`GC之`后,使用的便是`Eden space`和`To Space`了,下次`GC`时会将存活对象复制到`From Space`,如此反复循环。 - 当对象在`Survivor`区躲过一次`GC`的话,其对象年龄便会加1,默认情况下,如果对象年龄达到15岁,就会移动到老年代中。 - -垃圾收集器 ---- +### 标记-清除算法(Mark-Sweep) +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/biaoji_qingchu.png) + +原理:从根集合节点进行扫描,标记出所有的存活对象,最后扫描整个内存空间并清除没有标记的对象(即死亡对象) + +适用场合: + +- 存活对象较多的情况下比较高效 +- 适用于年老代(即旧生代) + +缺点: + +- 标记清除算法带来的一个问题是会存在大量的空间碎片,因为回收后的空间是不连续的,这样给大对象分配内存的时候可能会提前触发full gc。 + +### 标记复制算法(mark-copy) +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/fuzhisuanfa.png) + +原理:思路也很简单,将内存对半分,总是保留一块空着(上图中的右侧),将左侧存活的对象(浅灰色区域)复制到右侧,然后左侧全部清空。避免了内存碎片问题,但是内存浪费很严重,相当于只能使用 50% 的内存。。从根集合节点进行扫描,标记出所有的存活对象,并将这些存活的对象复制到一块儿新的内存(图中下边的那一块儿内存)上去,之后将原来的那一块儿内存(图中上边的那一块儿内存)全部回收掉 + +适用场合: + +- 存活对象较少的情况下比较高效 +- 扫描了整个空间一次(标记存活对象并复制移动) +- 适用于年轻代(即新生代):基本上98%的对象是”朝生夕死”的,存活下来的会很少 + +缺点: + +- 需要一块儿空的内存空间 +- 需要复制移动对象 + +### 标记-整理算法(Mark-Compact) +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/biaoji_zhengli.png) + +原理:从根集合节点进行扫描,标记出所有的存活对象,最后扫描整个内存空间并清除没有标记的对象(即死亡对象)(可以发现前边这些就是标记-清除算法的原理),清除完之后,将所有的存活对象左移到一起。 避免了上述两种算法的缺点,将垃圾对象清理掉后,同时将剩下的存活对象进行整理挪动(类似于 windows 的磁盘碎片整理),保证它们占用的空间连续,这样就避免了内存碎片问题,但是整理过程也会降低 GC 的效率。 + +适用场合: + +- 用于年老代(即旧生代) + +缺点: + +- 需要移动对象,若对象非常多而且标记回收后的内存非常不完整,可能移动这个动作也会耗费一定时间 +- 扫描了整个空间两次(第一次:标记存活对象;第二次:清除没有标记的对象) + +优点: + +- 不会产生内存碎片 + +### 分代收集算法(Generational Collection) + +上述三种算法,每种都有各自的优缺点,都不完美。在现代 JVM 中,往往是综合使用的,经过大量实际分析,发现内存中的对象,大致可以分为两类:有些生命周期很短,比如一些局部变量 / 临时对象,而另一些则会存活很久,典型的比如 websocket 长连接中的 connection 对象,如下图: + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/62226d097ac54148804119b4c239c802.png) + +纵向 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 三个区。 + +结合我们经常使用的一些 jvm 调优参数后,一些参数能影响的各区域内存大小值,示意图如下: + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/d4e6583cd0ecfa60622526e7a4f13634.png) + +注:jdk8 开始,用 MetaSpace 区取代了 Perm 区(永久代),所以相应的 jvm 参数变成 -XX:MetaspaceSize 及 -XX:MaxMetaspaceSize。 + +以 Hotspot 为例,我们来分析下 GC 的主要过程: + +刚开始时,对象分配在 eden 区,s0(即:from)及 s1(即:to)区,几乎是空着。 + +![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/8dc1423cc9f85658a946e204dc85ec18.png) + +随着应用的运行,越来越多的对象被分配到 eden 区。 + +![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/c9138916377192a8a2590aca3e888049.png) + +当 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 区就变成了空的了。 + +![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/f21d18a0736d0976c4ecf14e82236cb2.png) + +继续,随着对象的不断分配,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)”及“晋升”。 + +对象在年青代的 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 后,如果对象还活着,晋升到老年代。 + +![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/6d24d96eb137f805c867736a750c2bd9.png) + +如果老年代,最终也放满了,就会发生 major GC(即 Full GC),由于老年代的的对象通常会比较多,因为标记 - 清理 - 整理(压缩)的耗时通常会比较长,会让应用出现卡顿的现象,这也是为什么很多应用要优化,尽量避免或减少 Full GC 的原因。 + +![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/ebad989a70f78a40c561df9943234441.png) + +注:上面的过程主要来自 oracle 官网的资料,但是有一个细节官网没有提到,如果分配的新对象比较大,eden 区放不下,但是 old 区可以放下时,会直接分配到 old 区(即没有晋升这一过程,直接到老年代了)。 + +下图引自阿里出品的《码出高效 -Java 开发手册》一书,梳理了 GC 的主要过程。 + +![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/e663bd3043c6b3465edc1e7313671d69.png) + + + +## 垃圾回收器 + +不算最新出现的神器 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 都是回收老年代的。 + + 垃圾收集算法是内存回收的理论基础,而垃圾收集器就是内存回收的具体实现。 下面介绍一下`HotSpot(JDK 7)`虚拟机提供的几种垃圾收集器,用户可以根据自己的需求组合出各个年代使用的收集器。 -- Serial/Serial Old - `Serial/Serial Old`收集器是最基本最古老的收集器,它是一个单线程收集器,并且在它进行垃圾收集时, - 必须暂停所有用户线程。`Serial`收集器是针对新生代的收集器,采用的是`Copying`算法,`Serial Old`收集器是针对老年代的收集器, - 采用的是`Mark-Compact`算法。它的优点是实现简单高效,但是缺点是会给用户带来停顿。 - -- ParNew - `ParNew`收集器是`Serial`收集器的多线程版本,使用多个线程进行垃圾收集。 - -- Parallel Scavenge - `Parallel Scavenge`收集器是一个新生代的多线程收集器(并行收集器),它在回收期间不需要暂停其他用户线程,其采用的是`Copying`算法, - 该收集器与前两个收集器有所不同,它主要是为了达到一个可控的吞吐量。 - -- Parallel Old - `Parallel Old`是`Parallel Scavenge`收集器的老年代版本(并行收集器),使用多线程和`Mark-Compact`算法。 - -- CMS - `CMS(Current Mark Sweep)`收集器是一种以获取最短回收停顿时间为目标的收集器,它是一种并发收集器,采用的是`Mark-Sweep`算法。 - -- G1 - `G1`收集器是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多`CPU`、多核环境。因此它是一款并行与并发收集器, - 并且它能建立可预测的停顿时间模型。 - - + +### Serial/Serial Old +`Serial/Serial Old`收集器是最基本最古老的收集器,它是一个单线程收集器,并且在它进行垃圾收集时, +必须暂停所有用户线程。`Serial`收集器是针对新生代的收集器,采用的是`Copying`算法,`Serial Old`收集器是针对老年代的收集器, +采用的是`Mark-Compact`算法。它的优点是实现简单高效,但是缺点是会给用户带来停顿。 + +### ParNew + +ParNew收集器是Serial收集器新生代的多线程实现,注意在进行垃圾回收的时候依然会stop the world,只是相比较Serial收集器而言它会运行多条进程进行垃圾回收。 + +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时间,尽快地完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。 + +### CMS + +全称:Concurrent Mark Sweep,从名字上看,就能猜出它是并发多线程的。这是 JDK 7 中广泛使用的收集器,有必要多说一下,借一张网友的图说话: + +![一文看懂JVM内存布局及GC原理](https://static001.infoq.cn/resource/image/e8/4e/e8b152cf510b06544c2a13bfb4fc564e.png) + +相对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 直接关联的下级对象不会很多,因此这个过程其实很快。 + +2)Concurrent Mark 并发标记:根据上一步的结果,继续向下标识所有关联的对象,直到这条链上的最尽头。这个过程是多线程的,虽然耗时理论上会比较长,但是其它工作线程并不会阻塞,没有 STW。 + +3)Remark 再标志:为啥还要再标记一次?因为第 2 步并没有阻塞其它工作线程,其它线程在标识过程中,很有可能会产生新的垃圾。 + +试想下,高铁上的垃圾清理员,从车厢一头开始吆喝“有需要扔垃圾的乘客,请把垃圾扔一下”,一边工作一边向前走,等走到车厢另一头时,刚才走过的位置上,可能又有乘客产生了新的空瓶垃圾。所以,要完全把这个车厢清理干净的话,她应该喊一下:所有乘客不要再扔垃圾了(STW),然后把新产生的垃圾收走。当然,因为刚才已经把收过一遍垃圾,所以这次收集新产生的垃圾,用不了多长时间(即:STW 时间不会很长)。 + +4)Concurrent Sweep:并行清理,这里使用多线程以“Mark Sweep- 标记清理”算法,把垃圾清掉,其它工作线程仍然能继续支行,不会造成卡顿。 + +等等,刚才我们不是提到过“标记清理”法,会留下很多内存碎片吗?确实,但是也没办法,如果换成“Mark Compact 标记 - 整理”法,把垃圾清理后,剩下的对象也顺便排整理,会导致这些对象的内存地址发生变化,别忘了,此时其它线程还在工作,如果引用的对象地址变了,就天下大乱了。 + +另外,由于这一步是并行处理,并不阻塞其它线程,所以还有一个副使用,在清理的过程中,仍然可能会有新垃圾对象产生,只能等到下一轮 GC,才会被清理掉。不过由于CMS收集器是基于标记清除算法实现的,会导致有大量的空间碎片产生,在为大对象分配内存的时候,往往会出现老年代还有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前开启一次Full GC。 + +为了解决这个问题,CMS收集器默认提供了一个-XX:+UseCMSCompactAtFullCollection收集开关参数(默认就是开启的),用于在CMS收集器进行FullGC完开启内存碎片的合并整理过程,内存整理的过程是无法并发的,这样内存碎片问题倒是没有了,不过停顿时间不得不变长。虚拟机设计者还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction参数用于设置执行多少次不压缩的FULL GC后跟着来一次带压缩的(默认值为0,表示每次进入Full GC时都进行碎片整理)。 + +不幸的是,它作为老年代的收集器,却无法与jdk1.4中已经存在的新生代收集器Parallel Scavenge配合工作,所以在jdk1.5中使用cms来收集老年代的时候,新生代只能选择ParNew或Serial收集器中的一个。ParNew收集器是使用-XX:+UseConcMarkSweepGC选项启用CMS收集器之后的默认新生代收集器,也可以使用-XX:+UseParNewGC选项来强制指定它。 + +虽然仍不完美,但是从这 4 步的处理过程来看,以往收集器中最让人诟病的长时间 STW,通过上述设计,被分解成二次短暂的 STW,所以从总体效果上看,应用在 GC 期间卡顿的情况会大大改善,这也是 CMS 一度十分流行的重要原因。 + +### 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毫秒。 + +在使用G1收集器时,Java堆的内存布局和其他收集器有很大的差别,它将这个Java堆分为多个大小相等的独立区域,虽然还保留新生代和老年代的概念,但是新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。 + +虽然G1看起来有很多优点,实际上CMS还是主流。 + + + +如下图,G1 将 heap 内存区,划分为一个个大小相等(1-32M,2 的 n 次方)、内存连续的 Region 区域,每个 region 都对应 Eden、Survivor 、Old、Humongous 四种角色之一,但是 region 与 region 之间不要求连续。 + +注:Humongous,简称 H 区是专用于存放超大对象的区域,通常 >= 1/2 Region Size,且只有 Full GC 阶段,才会回收 H 区,避免了频繁扫描、复制 / 移动大对象。 + +所有的垃圾回收,都是基于 1 个个 region 的。JVM 内部知道,哪些 region 的对象最少(即:该区域最空),总是会优先收集这些 region(因为对象少,内存相对较空,肯定快),这也是 Garbage-First 得名的由来,G 即是 Garbage 的缩写, 1 即 First。 + +![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/f1a1bdecf4e56a4440707ce073d73f61.png) + +**G1 Young GC** + +young GC 前: + +![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/28b9077c545675b934d64ee643e30a9a.png) + +young GC 后: + +![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/be46c92240c7fd6dfcc6c11a48e4edd9.png) + +理论上讲,只要有一个 Empty Region(空区域),就可以进行垃圾回收。 + +![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/28ca063660f168b462ed8466faa77e68.png) + +由于 region 与 region 之间并不要求连续,而使用 G1 的场景通常是大内存,比如 64G 甚至更大,为了提高扫描根对象和标记的效率,G1 使用了二个新的辅助存储结构: + +Remembered Sets:简称 RSets,用于根据每个 region 里的对象,是从哪指向过来的(即:谁引用了我),每个 Region 都有独立的 RSets。(Other Region -> Self Region)。 + +Collection Sets :简称 CSets,记录了等待回收的 Region 集合,GC 时这些 Region 中的对象会被回收(copied or moved)。 + +RSets 的引入,在 YGC 时,将年青代 Region 的 RSets 做为根对象,可以避免扫描老年代的 region,能大大减轻 GC 的负担。注:在老年代收集 Mixed GC 时,RSets 记录了 Old->Old 的引用,也可以避免扫描所有 Old 区。 + + + +### ZGC (截止目前史上最好的 GC 收集器) + +在 G1 的基础上,做了很多改进(JDK 11 开始引入) + +#### 动态调整大小的 Region + +G1 中每个 Region 的大小是固定的,创建和销毁 Region,可以动态调整大小,内存使用更高效。 + +![一文看懂JVM内存布局及GC原理](https://static001.infoq.cn/resource/image/4f/03/4fd12cf96cf020e56d071161fa56b603.png) + +#### 不分代,干掉了 RSets + +G1 中每个 Region 需要借助额外的 RSets 来记录“谁引用了我”,占用了额外的内存空间,每次对象移动时,RSets 也需要更新,会产生开销。 + +注:ZGC 没有为止,没有实现分代机制,每次都是并发的对所有 region 进行回收,不象 G1 是增量回收,所以用不着 RSets。不分代的带来的可能性能下降,会用下面马上提到的 Colored Pointer && Load Barrier 来优化。 + +#### 带颜色的指针 Colored Pointer + +![一文看懂JVM内存布局及GC原理](https://static001.infoq.cn/resource/image/0e/5a/0e071b9c1124d0b7b09150d960c2925a.png) + +这里的指针类似 java 中的引用,意为对某块虚拟内存的引用。ZGC 采用了 64 位指针(注:目前只支持 linux 64 位系统),将 42-45 这 4 个 bit 位置赋予了不同的含义,即所谓的颜色标志位,也换为指针的 metadata。 + +finalizable 位:仅 finalizer(类比 c++ 中的析构函数)可访问; + +remap 位:指向对象当前(最新)的内存地址,参考下面提到的 relocation; + +marked0 && marked1 位:用于标志可达对象; + +这 4 个标志位,同一时刻只会有 1 个位置是 1。每当指针对应的内存数据发生变化,比如内存被移动,颜色会发生变化。 + +#### 读屏障 Load Barrier + +传统 GC 做标记时,为了防止其它线程在标记期间修改对象,通常会简单的 STW。而 ZGC 有了 Colored Pointer 后,引入了所谓的读屏障,当指针引用的内存正被移动时,指针上的颜色就会变化,ZGC 会先把指针更新成最新状态,然后再返回。(大家可以回想下 java 中的[ volatile 关键字](https://www.cnblogs.com/yjmyzz/p/6994796.html),有异曲同工之妙),这样仅读取该指针时可能会略有开销,而不用将整个 heap STW。 + +#### 重定位 relocation + +![一文看懂JVM内存布局及GC原理](https://static001.infoq.cn/resource/image/0e/dd/0ee7dc65b940fe54d0b67a217174d2dd.png) + +如上图,在标记过程中,先从 Roots 对象找到了直接关联的下级对象 1,2,4。 + +![一文看懂JVM内存布局及GC原理](https://static001.infoq.cn/resource/image/fd/e2/fd2797de4c1ba3b3f8e3b6b871d773e2.png) + +然后继续向下层标记,找到了 5,8 对象, 此时已经可以判定 3,6,7 为垃圾对象。 + +![一文看懂JVM内存布局及GC原理](https://static001.infoq.cn/resource/image/95/28/9524b9346adfa6f78283b73b1b201728.png) + +如果按常规思路,一般会将 8 从最右侧的 Region 移动或复制到中间的 Region,然后再将中间 Region 的 3 干掉,最后再对中间 Region 做压缩 compact 整理。但 ZGC 做得更高明,它直接将 4,5 复制到了一个空的新 Region 就完事了,然后中间的 2 个 Region 直接废弃,或理解为“释放”,做为下次回收的“新”Region。这样的好处是避免了中间 Region 的 compact 整理过程。 + +![一文看懂JVM内存布局及GC原理](https://static001.infoq.cn/resource/image/7b/22/7b18ada0e14c1fb65eabd73d7760a422.png) + +最后,指针重新调整为正确的指向(即:remap),而且上一阶段的 remap 与下一阶段的 mark 是混在一起处理的,相对更高效。 + +Remap 的流程图如下: + +![一文看懂JVM内存布局及GC原理](https://static001.infoq.cn/resource/image/8f/c7/8f02e87ca3f90da08edb414e174fd8c7.png) + +#### 多重映射 Multi-Mapping + +这个优化,说实话没完全看懂,只能谈下自己的理解(如果有误,欢迎指正)。虚拟内存与实际物理内存,OS 会维护一个映射关系,才能正常使用。如下图: + +![一文看懂JVM内存布局及GC原理](https://static001.infoq.cn/resource/image/4c/c4/4ce9d8741ffd7bcde179ed56ac56abc4.png) + +zgc 的 64 位颜色指针,在解除映射关系时,代价较高(需要屏蔽额外的 42-45 的颜色标志位)。考虑到这 4 个标志位,同 1 时刻,只会有 1 位置成 1(如下图),另外 finalizable 标志位,永远不希望被解除映射绑定(可不用考虑映射问题)。 + +所以剩下 3 种颜色的虚拟内存,可以都映射到同 1 段物理内存。即映射复用,或者更通俗点讲,本来 3 种不同颜色的指针,哪怕 0-41 位完全相同,也需要映射到 3 段不同的物理内存,现在只需要映射到同 1 段物理内存即可。 + +![一文看懂JVM内存布局及GC原理](https://static001.infoq.cn/resource/image/ac/49/ac0d5ac0f5f32a7c1c23bde7f513b649.png) + +![一文看懂JVM内存布局及GC原理](https://static001.infoq.cn/resource/image/44/2c/445295bc8132bf5aa33fd5aa6cc3c02c.png) + +#### 支持[ NUMA 架构](https://baike.baidu.com/item/NUMA/6906025?fr=aladdin) + +NUMA 是一种多核服务器的架构,简单来讲,一个多核服务器(比如 2core),每个 cpu 都有属于自己的存储器,会比访问另一个核的存储器会慢很多(类似于就近访问更快)。 + +相对之前的 GC 算法,ZGC 首次支持了 NUMA 架构,申请堆内存时,判断当前线程属是哪个 CPU 在执行,然后就近申请该 CPU 能使用的内存。 + +**小结**:革命性的 ZGC 经过上述一堆优化后,每次 GC 总体卡顿时间按官方说法 <10ms。注:启用 zgc,需要设置 -XX:+UnlockExperimentalVMOptions -XX:+UseZGC。 + + + + + + + +# 与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) 的时间用于垃圾收集。 + + + +参考: + +- [一文看懂 JVM 内存布局及 GC 原理](https://www.infoq.cn/article/3WyReTKqrHIvtw4frmr3) + + + --- - 邮箱 :charon.chui@gmail.com 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 2ec2cda3..5b1f37ab 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" @@ -1,30 +1,291 @@ -volatile和Synchronized区别 -=== +# volatile和Synchronized -- volatile - 作用:使变量在多个线程间可见(可见性) - `Java`语言规范中指出:为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而在这个过程中,变量的新值对其他线程是不可见的.而且只当线程进入或者离开同步代码块时才与共享成员变量 -的原始值对比。这样当多个线程同时与某个对象交互时,就必须要注意到要让线程及时的得到共享成员变量的变化。 +## Java内存模型(Java Memory Model ,JMM) -也就是说每个线程都有一个自己的本地内存空间,在线程执行时,先把变量从主内存读取到线程自己的本地内存空间,然后再对该变量进行操作,当对该变量操作完后,在某个时间再把变量刷新回主内存 +**Java内存模型**(java Memory Model)描述了Java程序中各种变量(**线程共享变量**)的访问规则,以及在JVM中将变量**存储到内存**和从**内存中读取出变量**这样的底层细节。 -而`volatile`关键字就是提示`JVM`:对于这个成员变量不能保存它的私有拷贝,而应直接与共享成员变量交互。 -使用建议:在两个或者更多的线程访问的成员变量上使用`volatile`。当要访问的变量已在`synchronized`代码块中,或者为常量时,不必使用。 -由于使用`volatile`屏蔽掉了`JVM`中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。 就跟`C`中的一样 禁止编译器进行优化. -注意:如果给一个变量加上`volatile`修饰符,就相当于:每一个线程中一旦这个值发生了变化就马上刷新回主存,使得各个线程取出的值相同。编译器不要对这个变量的读、写操作做优化。但是值得注意的是,除了对`long`和`double`的简单操作之外,`volatile`并不能提供原子性。 -所以,就算你将一个变量修饰为`volatile`,但是对这个变量的操作并不是原子的,在并发环境下,还是不能避免错误的发生! -- synchronized - `synchronized`为一段操作或内存进行加锁,它具有互斥性。当线程要操作被`synchronized`修饰的内存或操作时,必须首先获得锁才能进行后续操作;但是在同一时刻只能有一个线程获得相同的一把锁(对象监视器),所以它只允许一个线程进行操作。 - 它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。 - - 当两个并发线程访问同一个对象中的这个`synchronized(this)`同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。 - - 然而,当一个线程访问`object`的一个`synchronized(this)`同步代码块时,另一个线程仍然可以访问该`object`中的非`synchronized(this)`同步代码块。 - - 尤其关键的是,当一个线程访问`object`的一个`synchronized(this)`同步代码块时,其他线程对`object`中所有其它`synchronized(this)`同步代码块的访问将被阻塞。 +计算机在执行程序的时候,每条指令都是在CPU中执行的,而执行的时候,又免不了要和数据打交道。而计算机上面的数据,是存放在主存当中的,也就是计算机的物理内存。但是随着CPU技术的发展,CPU的执行速度越来越快。而由于内存的技术并没有太大的变化,所以从内存中读取和写入数据的过程和CPU的执行速度比起来差距就会越来越大,这就导致CPU每次操作内存都要耗费很多等待时间。可是,不能因为内存的读写速度慢,就不发展CPU技术了,所以人们想出来了一个好的办法,就是在CPU和内存之间增加高速缓存。缓存的概念大家都知道,就是保存一份数据拷贝。他的特点是速度快,内存小,并且昂贵。 + +那么,程序的执行过程就变成了:当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。而随着CPU能力的不断提升,一层缓存就慢慢的无法满足要求了,就逐渐的衍生出多级缓存。按照数据读取顺序和与CPU结合的紧密程度,CPU缓存可以分为一级缓存(L1),二级缓存(L3),部分高端CPU还具有三级缓存(L3),每一级缓存中所储存的全部数据都是下一级缓存的一部分。 + +这三种缓存的技术难度和制造成本是相对递减的,所以其容量也是相对递增的。 + +那么,在有了多级缓存之后,程序的执行就变成了: + +当CPU要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找。 + + + +> 这就像一家创业公司,刚开始,创始人和员工之间工作关系其乐融融,但是随着创始人的能力和野心越来越大,逐渐和员工之间出现了差距,普通员工原来越跟不上CEO的脚步。老板的每一个命令,传到到基层员工之后,由于基层员工的理解能力、执行能力的欠缺,就会耗费很多时间。这也就无形中拖慢了整家公司的工作效率。 +> +> 之后,这家公司开始设立中层管理人员,管理人员直接归CEO领导,领导有什么指示,直接告诉管理人员,然后就可以去做自己的事情了。管理人员负责去协调底层员工的工作。因为管理人员是了解手下的人员以及自己负责的事情的。所以,大多数时候,公司的各种决策,通知等,CEO只要和管理人员之间沟通就够了。 +> +> 随着公司越来越大,老板要管的事情越来越多,公司的管理部门开始改革,开始出现高层,中层,底层等管理者。一级一级之间逐层管理。 +> +> 公司也分很多种,有些公司只有一个大Boss,他一个人说了算。但是有些公司有比如联席总经理、合伙人等机制。 +> +> 单核CPU就像一家公司只有一个老板,所有命令都来自于他,那么就只需要一套管理班底就够了。 +> +> 多核CPU就像一家公司是由多个合伙人共同创办的,那么,就需要给每个合伙人都设立一套供自己直接领导的高层管理人员,多个合伙人共享使用的是公司的底层员工。 +> +> 还有的公司,不断壮大,开始差分出各个子公司。各个子公司就是多个CPU了,互相使用没有共用的资源。互不影响。 + +随着计算机能力不断提升,开始支持多线程。那么问题就来了。我们分别来分析下单线程、多线程在单核CPU、多核CPU中的影响。 + +**单线程。**cpu核心的缓存只被一个线程访问。缓存独占,不会出现访问冲突等问题。 + +**单核CPU,多线程。**进程中的多个线程会同时访问进程中的共享数据,CPU将某块内存加载到缓存后,不同线程在访问相同的物理地址的时候,都会映射到相同的缓存位置,这样即使发生线程的切换,缓存仍然不会失效。但由于任何时刻只能有一个线程在执行,因此不会出现缓存访问冲突。 + +**多核CPU,多线程。**每个核都至少有一个L1 缓存。多个线程访问进程中的某个共享内存,且这多个线程分别在不同的核心上执行,则每个核心都会在各自的caehe中保留一份共享内存的缓冲。由于多核是可以并行的,可能会出现多个线程同时写各自的缓存的情况,而各自的cache之间的数据就有可能不同。 + +在CPU和主存之间增加缓存,在多线程场景下就可能存在**缓存一致性问题**,也就是说,在多核CPU中,每个核的自己的缓存中,关于同一个数据的缓存内容可能不一致。 + +> 如果这家公司的命令都是串行下发的话,那么就没有任何问题。 +> +> 如果这家公司的命令都是并行下发的话,并且这些命令都是由同一个CEO下发的,这种机制是也没有什么问题。因为他的命令执行者只有一套管理体系。 +> +> 如果这家公司的命令都是并行下发的话,并且这些命令是由多个合伙人下发的,这就有问题了。因为每个合伙人只会把命令下达给自己直属的管理人员,而多个管理人员管理的底层员工可能是公用的。 +> +> 比如,合伙人1要辞退员工a,合伙人2要给员工a升职,升职后的话他再被辞退需要多个合伙人开会决议。两个合伙人分别把命令下发给了自己的管理人员。合伙人1命令下达后,管理人员a在辞退了员工后,他就知道这个员工被开除了。而合伙人2的管理人员2这时候在没得到消息之前,还认为员工a是在职的,他就欣然的接收了合伙人给他的升职a的命令。 + +### 处理器优化和指令重排 + +上面提到在在CPU和主存之间增加缓存,在多线程场景下会存在**缓存一致性问题**。除了这种情况,还有一种硬件问题也比较重要。那就是为了使处理器内部的运算单元能够尽量的被充分利用,处理器可能会对输入代码进行乱序执行处理。这就是**处理器优化**。 + +除了现在很多流行的处理器会对代码进行优化乱序处理,很多编程语言的编译器也会有类似的优化,比如Java虚拟机的即时编译器(JIT)也会做**指令重排**。 + +可想而知,如果任由处理器优化和编译器对指令重排的话,就可能导致各种各样的问题。 + +> 关于员工组织调整的情况,如果允许人事部在接到多个命令后进行随意拆分乱序执行或者重排的话,那么对于这个员工以及这家公司的影响是非常大的。 + + + +并发编程,为了保证数据的安全,需要满足以下三个特性: + +**原子性**是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。 + +**可见性**是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。 + +**有序性**即程序执行的顺序按照代码的先后顺序执行。 + +有没有发现,**缓存一致性问题**其实就是**可见性问题**。而**处理器优化**是可以导致**原子性问题**的。**指令重排**即会导致**有序性问题**。所以,后文将不再提起硬件层面的那些概念,而是直接使用大家熟悉的原子性、可见性和有序性。 + +前面提到的,缓存一致性问题、处理器器优化的指令重排问题是硬件的不断升级导致的。那么,有没有什么机制可以很好的解决上面的这些问题呢? + +最简单直接的做法就是废除处理器和处理器的优化技术、废除CPU缓存,让CPU直接和主存交互。但是,这么做虽然可以保证多线程下的并发问题。但是,这就有点因噎废食了。 + +所以,为了保证并发编程中可以满足原子性、可见性及有序性。有一个重要的概念,那就是——内存模型。 + +**为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范。**通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。它与处理器有关、与缓存有关、与并发有关、与编译器也有关。他解决了CPU多级缓存、处理器优化、指令重排等导致的内存访问问题,保证了并发场景下的一致性、原子性和有序性。 + +内存模型解决并发问题主要采用两种方式:**限制处理器优化**和**使用内存屏障**。 + +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屏障指令 + +当volatile修饰一个变量i在线程1中从1发生改变成2时,这时线程1会做两件事: + +1. **更新主内存。** +2. **向CPU总线发送一个修改信号。** + + + +**这时监听CPU总线的处理器会收到这个修改信号后,如果发现修改的数据自己缓存了,就把自己缓存的数据失效掉。这样其它线程访问到这段缓存时知道缓存数据失效了,需要从主内存中获取。这样所有线程中的共享变量i就达到了一致性。** + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/java_mm_1.jpg) + +**所以volatile也可以看作线程间通信的一种廉价方式。** + + + + +### volatile关键字的非原子性 + +所谓原子性,就是某系列的操作步骤要么全部执行,要么都不执行。 + +比如,变量的自增操作 i++,分三个步骤: + +- 从内存中读取出变量 i 的值 + +- 将i的值加1 + +- 将加1后的值写回内存 + +这说明 i++ 并不是一个原子操作。因为,它分成了三步,有可能当某个线程执行到了第②时被中断了,那么就意味着只执行了其中的两个步骤,没有全部执行。 + +volatile修饰的变量并不保证对它的操作(自增)具有原子性。(对于自增操作,可以使用JAVA的原子类AutoicInteger类保证原子自增) + +比如,假设i自增到5,线程A从主内存中读取i,值为5,将它存储到自己的线程空间中,执行加1操作,值为6。此时,CPU切换到线程B执行,从主从内存中读取变量i的值。由于线程A还没有来得及将加1后的结果写回到主内存,线程B就已经从主内存中读取了i,因此,线程B读到的变量i值还是5。 + +相当于线程B读取的是已经过时的数据了,从而导致线程不安全性。这种情形在《Effective JAVA》中称之为“安全性失败” + +**综上,仅靠volatile不能保证线程的安全性。(原子性)** + + + +### volatile禁止指令重排序 + +代码书写的顺序与实际执行的顺序不同,指令重排序是编译器或处理器为了提高程序性能而做的优化 + +1. **编译器优化的重排序(编译器优化)** +2. **指令级并行重排序(处理器优化)** +3. **内存系统的重排序(处理器优化)** + +volatile禁止指令重排序,典型的应用是单例模式中的双重检查机制。 + +由于synchronized是一个重量级锁,会影响运行效率。双重检查机制中,先判断单例变量是否为null,再进入同步代码块。这样只在第一次获取单例变量时,会有效率影响。 + +```java +public class DoubleCheckSingleton { + + private volatile static DoubleCheckSingleton singleton; + + private DoubleCheckSingleton() {} + + public static DoubleCheckSingleton getInstance() { + if(singleton == null) { + synchronized (singleton) { + if(singleton == null) { + singleton = new DoubleCheckSingleton(); + } + } + } + return singleton; + } + +} +``` + +单例变量需要用volatile修饰,否则,在new单例对象时会出现问题。使用new来创建一个对象可以分解为如下的3行伪代码: + +```java +memory = allocate(); //1.分配对象的内存空间 +ctorInstance(memory); //2.初始化对象 +instance = memory; //3.设置instance指向刚分配的内存地址 +``` + +上面3行伪代码中的2和3之间可能会发生重排序,排序后的执行顺序如下: + +```java +memory = allocate(); //1.分配对象的内存空间 +instance = memory; //3.设置instance指向刚分配的内存地址,此时对象还没有初始化,但instance == null 判断为false +ctorInstance(memory); //2.初始化对象 +``` + +指令重排序后,在多线程情况下,可能会发生A线程正在new对象,执行了3,但还没有执行2。此时B线程进入方法获取单例对象,执行同步代码块外的非空判断,发现变量非空,但此时对象还未初始化,B线程获取到的是一个未被初始化的对象。使用volatile修饰后,禁止指令重排序。即,先初始化对象后,再设置instance指向刚分配的内存地址。这样就就不存在获取到未被初始化的对象。 + + + +## synchronized + +**synchronized实现同步的基础是:Java中的每个对象都可作为锁。所以synchronized锁的都对象,只不过不同形式下锁的对象不一样。** + +- **对于普通同步方法,锁的是当前实例对象。** +- **对于静态同步方法,锁的是当前类的Class对象。** +- **对于同步方法块,锁是Synchronized括号里配置的对象。** + +`synchronized`为一段操作或内存进行加锁,它具有互斥性。当线程要操作被`synchronized`修饰的内存或操作时,必须首先获得锁才能进行后续操作;但是在同一时刻只能有一个线程获得相同的一把锁(对象监视器),所以它只允许一个线程进行操作。 +它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。 + +- 当两个并发线程访问同一个对象中的这个`synchronized(this)`同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。 +- 然而,当一个线程访问`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缺点 + +#### 有性能损耗 + +虽然在JDK 1.6中对synchronized做了很多优化,如如适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等,但是他毕竟还是一种锁。以上这几种优化,都是尽量想办法避免对Monitor进行加锁,但是,并不是所有情况都可以优化的,况且就算是经过优化,优化的过程也是有一定的耗时的。所以,无论是使用同步方法还是同步代码块,在同步操作之前还是要进行加锁,同步操作之后需要进行解锁,这个加锁、解锁的过程是要有性能损耗的。 + +Monitor其实是一种同步工具,也可以说是一种同步机制,它通常被描述为一个对象,主要特点是: + +> 对象的所有方法都被“互斥”的执行。好比一个Monitor只有一个运行“许可”,任一个线程进入任何一个方法都需要获得这个“许可”,离开时把许可归还。 +> +> 通常提供singal机制:允许正持有“许可”的线程暂时放弃“许可”,等待某个谓词成真(条件变量),而条件成立后,当前进程可以“通知”正在等待这个条件变量的线程,让他可以重新去获得运行许可。 + + + +#### 产生阻塞 + +基于Monitor对象,当多个线程同时访问一段同步代码时,首先会进入Entry Set,当有一个线程获取到对象的锁之后,才能进行The Owner区域,其他线程还会继续在Entry Set等待。并且当某个线程调用了wait方法后,会释放锁并进入Wait Set等待。 + +![img](http://47.103.216.138/wp-content/uploads/2019/08/15660298698995.jpg) + +所以,synchronize实现的锁本质上是一种阻塞锁,也就是说多个线程要排队访问同一个共享对象。 + + + +## volatile与Synchronized的区别: + + + +- synchronized通过加锁的方式,使得其在需要原子性、可见性和有序性这三种特性的时候都可以作为其中一种解决方案,看起来是“万能”的。的确,大部分并发控制操作都能使用synchronized来完成。 + +- volatile通过在volatile变量的操作前后插入内存屏障的方式,保证了变量在并发场景下的可见性和有序性。 + +- volatile关键字是无法保证原子性的,而synchronized通过monitorenter和monitorexit两个指令,可以保证被synchronized修饰的代码在同一时间只能被一个线程访问,即可保证不会出现CPU时间片在多个线程间切换,即可保证原子性 + +- volatile是变量修饰符,而synchronized则作用于一段代码或方法。 +- volatile只是在线程内存和“主”内存间同步某个变量的值;而synchronized通过锁定和解锁某个监视器同步所有变量的值。显然synchronized要比volatile消耗更多资源。 + +一句话,那什么时候才能用volatile关键字呢?(千万记住了,重要事情说三遍,感觉这句话过时了) + +> 如果写入变量值不依赖变量当前值,那么就可以用 volatile +> +> 如果写入变量值不依赖变量当前值,那么就可以用 volatile +> +> 如果写入变量值不依赖变量当前值,那么就可以用 volatile + +比如上面 count++ ,是获取-计算-写入三步操作,也就是依赖当前值的,所以不能靠volatile 解决问题。 + + + +参考: + +- [再有人问你Java内存模型是什么,就把这篇文章发给他](https://www.hollischuang.com/archives/2550) + -- 区别: - - `volatile`是变量修饰符,而`synchronized`则作用于一段代码或方法。 - - `volatile`只是在线程内存和“主”内存间同步某个变量的值;而`synchronized`通过锁定和解锁某个监视器同步所有变量的值。显然`synchronized`要比`volatile`消耗更多资源。 --- diff --git "a/JavaKnowledge/\346\225\260\346\215\256\345\212\240\345\257\206\345\217\212\350\247\243\345\257\206.md" "b/JavaKnowledge/\346\225\260\346\215\256\345\212\240\345\257\206\345\217\212\350\247\243\345\257\206.md" index d3832e6b..fa29a7b1 100644 --- "a/JavaKnowledge/\346\225\260\346\215\256\345\212\240\345\257\206\345\217\212\350\247\243\345\257\206.md" +++ "b/JavaKnowledge/\346\225\260\346\215\256\345\212\240\345\257\206\345\217\212\350\247\243\345\257\206.md" @@ -21,19 +21,17 @@ - DES -`DES`全称为`Data Encryption Standard`,即数据加密标准,是一种使用密钥加密的块算法, -1976年被美国联邦政府的国家标准局确定为联邦资料处理标准(`FIPS`),随后在国际上广泛流传开来。 - -`DES`算法的入口参数有三个:`Key`、`Data`、`Mode`。其中`Key`为8个字节共64位,是`DES`算法的工作密钥;`Data`也为8个字节64位, -是要被加密或被解密的数据;`Mode`为`DES`的工作方式,有两种:加密或解密。 -`DES`算法把64位的明文输入块变为64位的密文输出块,它所使用的密钥也是64位。 - + DES`全称为`Data Encryption Standard`,即数据加密标准,是一种使用密钥加密的块算法, + 1976年被美国联邦政府的国家标准局确定为联邦资料处理标准(`FIPS`),随后在国际上广泛流传开来。` + `DES`算法的入口参数有三个:`Key`、`Data`、`Mode`。其中`Key`为8个字节共64位,是`DES`算法的工作密钥;`Data`也为8个字节64位, + 是要被加密或被解密的数据;`Mode`为`DES`的工作方式,有两种:加密或解密。 + `DES`算法把64位的明文输入块变为64位的密文输出块,它所使用的密钥也是64位。 - AES -`AES`全程为`Advanced Encryption Standard`,即高级加密标准,在密码学中又称`Rijndael`加密法, -是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的`DES`,已经被多方分析且广为全世界所使用。 -`AES`加密数据块分组长度必须为128比特,密钥长度可以是128比特、192比特、256比特中的任意一个(如果数据块及密钥长度不足时,会补齐) + `AES`全程为`Advanced Encryption Standard`,即高级加密标准,在密码学中又称`Rijndael`加密法, + 是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的`DES`,已经被多方分析且广为全世界所使用。 + `AES`加密数据块分组长度必须为128比特,密钥长度可以是128比特、192比特、256比特中的任意一个(如果数据块及密钥长度不足时,会补齐) From afab78854925643c9508f8d2da2bcd816f98b1d3 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 10 Jul 2020 15:07:22 +0800 Subject: [PATCH 027/213] update notes --- ...37\347\220\206\345\210\206\346\236\220.md" | 32 +++- ...12\346\234\211\345\272\217\346\200\247.md" | 171 ++++++++++++++++++ ...14Synchronized\345\214\272\345\210\253.md" | 17 +- ...12\346\234\211\345\272\217\346\200\247.md" | 74 -------- 4 files changed, 205 insertions(+), 89 deletions(-) create mode 100644 "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" delete mode 100644 "JavaKnowledge/\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" 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 63b0d41f..5ebc846c 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" @@ -292,6 +292,7 @@ public V get(Object key) { // 使用红黑树树而不链表的容器计数阈值 static final int TREEIFY_THRESHOLD = 8; // 用于在执行过程中取消使用红黑树(拆分)箱调整大小操作的箱计数阈值, + // 为啥这里转成红黑树是8,而从红黑树转成链表是6? 在hash函数设计合理的情况下,发生hash碰撞8次的几率为百万分之6,概率说话。。因为8够用了,至于为什么转回来是6,因为如果hash碰撞次数在8附近徘徊,会一直发生链表和红黑树的转化,为了预防这种情况的发生。 static final int UNTREEIFY_THRESHOLD = 6; // 哈希桶,存储数据的数组 transient Node[] table; @@ -337,6 +338,17 @@ static final int hash(Object key) { } ``` +hash函数是先拿到通过key 的hashcode,是32位的int值,然后让hashcode的高16位和低16位进行异或操作。为什么要这样设计呢? 主要有两个原因 : + +1. 一定要尽可能降低hash碰撞,越分散越好。 +2. 算法一定要尽可能高效,因为这是高频操作,因此采用位运算。 + +因为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,但是&比%具有更高的效率。 当length总是2的倍数时,h & (length-1) 将是一个非常巧妙的设计: @@ -709,8 +721,9 @@ final Node getNode(int hash, Object key) { 1. JDK8为红黑树 + 链表 + 数组的形式,当桶内元素大于8时,便会树化。 2. hash值的计算方式不同 (jdk 8 简化)。 3. JDK7中table在创建hashmap时分配空间,而8中在put的时候分配。 -4. 在发生冲突,插入链中时,7 是头插法,8 是尾插法 +4. 链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7将新元素放到数组中,原始节点作为新节点的后继节点,1.8遍历链表,将元素放置到链表的最后;因为头插法会使链表发生反转,多线程环境下会产生环; 5. 在resize操作中,7 需要重新进行index的计算,而8不需要,通过判断相应的位是0还是1,要么依旧是原index,要么是oldCap + 原 index。 +6. 在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容; @@ -721,8 +734,6 @@ 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最后四位。这种算法在保持分布均匀之外,效率也非常高。 - - 2. 为什么需要使用加载因子,为什么需要扩容呢? 因为如果填充比很大,说明利用的空间很多,如果一直不进行扩容的话,链表就会越来越长,这样查找的效率很低,因为链表的长度很大(当然最新版本使用了红黑树后会改进很多),扩容之后,将原来链表数组的每一个链表分成奇偶两个子链表分别挂在新链表数组的散列位置,这样就减少了每个链表的长度,增加查找效率。HashMap 本来是以空间换时间,所以填充比没必要太大。但是填充比太小又会导致空间浪费。如果关注内存,填充比可以稍大,如果主要关注查找性能,填充比可以稍小。 @@ -730,11 +741,8 @@ final Node getNode(int hash, Object key) { 3. 为什么 HashMap 是线程不安全的,实际会如何体现? - 如果多个线程同时使用put方法添加元素,假设正好存在两个put的key发生了碰撞 (hash值一样),那么根据HashMap的实现,这两个key会添加到数组的同一个位置,这样最终就会发生其中一个线程的put的数据被覆盖。 - - - 如果多个线程同时检测到元素个数超过数组大小 * loadFactor。这样会发生多个线程同时对hash数组进行扩容,都在重新计算元素位置以及复制数据,但是最终只有一个线程扩容后的数组会赋给table,也就是说其他线程的都会丢失,并且各自线程put的数据也丢失。且会引起死循环的错误。 - - - +- 如果多个线程同时检测到元素个数超过数组大小 * loadFactor。这样会发生多个线程同时对hash数组进行扩容,都在重新计算元素位置以及复制数据,但是最终只有一个线程扩容后的数组会赋给table,也就是说其他线程的都会丢失,并且各自线程put的数据也丢失。且会引起死循环的错误。 + 4. 与HashTable的区别 Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,它的并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。 @@ -745,8 +753,16 @@ final Node getNode(int hash, Object key) { - HashTable取哈希桶下标是直接用模运算%.(因为其默认容量也不是2的n次方。所以也无法用位运算替代模运算) - 扩容时,新容量是原来的2倍+1。int newCapacity = (oldCapacity << 1) + 1; - Hashtable是Dictionary的子类同时也实现了Map接口,HashMap是Map接口的一个实现类 + +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(旧数组的容量)。 + + 参考: 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" new file mode 100644 index 00000000..1f3a021f --- /dev/null +++ "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" @@ -0,0 +1,171 @@ +# Java并发编程之原子性、可见性以及有序性 + + + +- 缓存导致的可见性问题 +- 线程切换带来的原子性问题 +- 编译优化带来的有序性问题 + + + +## 原子性(Atomicity) + +众所周知,原子是构成物质的基本单位,所以原子代表着不可分。 +即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。 +最简单的一个例子就是银行转账问题,赋值或者`return`。比如`a = 1;`和 `return a;`这样的操作都具有原子性 +原子性不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作! +加锁可以保证复合语句的原子性,Java中提供了两个高级指令 `monitorenter`和 `monitorexit`,也就是对应的synchronized同步锁来保证原子性。 + +非原子性操作 +类似`a += b`这样的操作不具有原子性,在某些`JVM`中`a += b`可能要经过这样三个步骤: + +- 取出`a`和`b` +- 计算`a+b` +- 将计算结果写入内存 + 如果有两个线程`t1`,`t2`在进行这样的操作。`t1`在第二步做完之后还没来得及把数据写回内存就被线程调度器中断了,于是`t2`开始执行,`t2`执行完毕后`t1`又把没有完成的第三步做完。这个时候就出现了错误, + 相当于`t2`的计算结果被无视掉了。所以上面的买碘片例子在同步`add`方法之前,实际结果总是小于预期结果的,因为很多操作都被无视掉了。 + 类似的,像`a++`这样的操作也都不具有原子性。所以在多线程的环境下一定要记得进行同步操作。 + +## 可见性(Visibility) + +可见性指的是当一个线程修改了共享变量后,其他线程能够立即得知这个修改。 + +在多核处理器中,如果多个线程对一个变量进行操作,但是这多个线程有可能被分配到多个处理器中运行,那么编译器会对代码进行优化,当线程要处理该变量时,多个处理器会将变量从主内存复制一份分别存储在自己的片上存储器中,等到进行完操作后,再赋值回主存。(这样做的好处是提高了运行的速度,因为在处理过程中多个处理器减少了同主内存通信的次数);同样在单核处理器中这样由于备份造成的问题同样存在!这样的优化带来的问题之一是变量可见性——如果线程`t1`与线程`t2`分别被安排在了不同的处理器上面,那么`t1`与`t2`对于变量`A`的修改时相互不可见,如果`t1`给`A`赋值,然后`t2`又赋新值,那么`t2`的操作就将`t1`的操作覆盖掉了,这样会产生不可预料的结果。所以,即使有些操作时原子性的,但是如果不具有可见性,那么多个处理器中备份的存在就会使原子性失去意义。 + +volatile、synchronized、final都可以解决可见性问题。 + +## 有序性(Ordering) + +有序性:即程序执行的顺序按照代码的先后顺序执行。 + +有序性简单来说就是程序代码执行的顺序是否按照我们编写代码的顺序执行,一般来说,为了提高性能,编译器和处理器会对指令做重排序,重排序分3类 + +- 编译器优化重排序,在不改变单线程程序语义的前提下,改变代码的执行顺序 +- 指令集并行的重排序,对于不存在数据依赖的指令,处理器可以改变语句对应指令的执行顺序来充分利用CPU资源 +- 内存系统的重排序,也就是前面说的CPU的内存乱序访问问题 + +也就是说,我们编写的源代码到最终执行的指令,会经过三种重排序。 + +比如编写时顺序如下的程序: + +```java +1. a = 5; +2. b = 20; +3. c = a + b; +``` + +编译器优化后执行的顺序可能变成: + +```java +1. b = 20; +2. a = 5; +3. c = a + b; +``` + +在这个例子中,编译器调整了语句的顺序,但是不影响程序的最终结果 + +在单例模式的实现上有一种双重检验锁定的方式(Double-checked Locking): + +```java +public class Singleton{ + private static Singleton instance; + public static Singleton getInstance(){ + if (instance == null){ + synchronized(Singleton.class){ + if(instance == null) { + instance = new Singleton(); + } + } + } + return instance; + } +} +``` + +我们先看 instance=newSingleton() 的未被编译器优化的操作 + +- 指令 1:分配一块内存 M; +- 指令 2:在内存 M 上初始化 Singleton 对象; +- 指令 3:然后 M 的地址赋值给 instance 变量。 + +编译器优化后的操作指令 + +- 指令 1:分配一块内存 M; +- 指令 2:将 M 的地址赋值给 instance 变量; +- 指令 3:然后在内存 M 上初始化 Singleton 对象。 + +现在有A,B两个线程,我们假设线程A先执行getInstance()方法,当执行编译器优化后的操作指令2时(此时候未完成对象的初始化),这时候发生了线程切换,那么线程B进入,刚好执行到第一次判断instance==nul会发现 instance不等于null了,所以直接返回instance,而此时的 instance 是没有初始化过的。 + + + +Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本身就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则来获得的,这个规则决定了持有同一个锁的两个同步块只能串行地进入。 + +## **先行发生原则:** + +如果Java内存模型中所有的有序性都只靠volatile和synchronized来完成,那么有一些操作将会变得很啰嗦,但是我们在编写Java并发代码的时候并没有感觉到这一点,这是因为Java语言中有一个“先行发生”(Happen-Before)的原则。这个原则非常重要,它是判断数据是否存在竞争,线程是否安全的主要依赖。 + +先行发生原则是指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; +boolean flag = false; +i = 1; //语句1 +flag = true; //语句2 +``` +上面代码定义了一个`int`型变量,定义了一个`boolean`类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么`JVM`在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗? +不一定,为什么呢?这里可能会发生指令重排序`(Instruction Reorder)`。 +下面解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢? +再看下面一个例子: + +```java +int a = 10; //语句1 +int r = 2; //语句2 +a = a + 3; //语句3 +r = a*a; //语句4 +``` +这段代码有4个语句,那么可能的一个执行顺序是: +语句2->语句1->语句3->语句4 +那么可能不可能是这个执行顺序呢?语句2->语句1->语句4->语句3,这是不可能的,因为处理器在进行重排序时是会考虑指令之间的数据依赖性, +如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。 + +虽然重排序不会影响单个线程内程序执行的结果,但是多线程呢?下面看一个例子: +```java +//线程1: +context = loadContext(); //语句1 +inited = true; //语句2 + +//线程2: +while(!inited ){ + sleep() +} +doSomethingwithconfig(context); +``` +上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此时线程2会以为初始化工作已经完成, +那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。 +从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。 + + + +--- +- 邮箱 :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 5b1f37ab..d447a6d3 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" @@ -1,10 +1,8 @@ # volatile和Synchronized -## Java内存模型(Java Memory Model ,JMM) - -**Java内存模型**(java Memory Model)描述了Java程序中各种变量(**线程共享变量**)的访问规则,以及在JVM中将变量**存储到内存**和从**内存中读取出变量**这样的底层细节。 - +## 内存模型 +内存模型:英文名 Memory Model,它是一个老古董了。它是与计算机硬件有关的一个概念。那么,我先介绍下它和硬件到底有啥关系。 计算机在执行程序的时候,每条指令都是在CPU中执行的,而执行的时候,又免不了要和数据打交道。而计算机上面的数据,是存放在主存当中的,也就是计算机的物理内存。但是随着CPU技术的发展,CPU的执行速度越来越快。而由于内存的技术并没有太大的变化,所以从内存中读取和写入数据的过程和CPU的执行速度比起来差距就会越来越大,这就导致CPU每次操作内存都要耗费很多等待时间。可是,不能因为内存的读写速度慢,就不发展CPU技术了,所以人们想出来了一个好的办法,就是在CPU和内存之间增加高速缓存。缓存的概念大家都知道,就是保存一份数据拷贝。他的特点是速度快,内存小,并且昂贵。 @@ -60,7 +58,7 @@ > 关于员工组织调整的情况,如果允许人事部在接到多个命令后进行随意拆分乱序执行或者重排的话,那么对于这个员工以及这家公司的影响是非常大的。 - +### 并发编程问题 并发编程,为了保证数据的安全,需要满足以下三个特性: @@ -82,6 +80,10 @@ 内存模型解决并发问题主要采用两种方式:**限制处理器优化**和**使用内存屏障**。 +## Java内存模型 + +**Java内存模型**(java Memory Model,JMM)描述了Java程序中各种变量(**线程共享变量**)的访问规则,以及在JVM中将变量**存储到内存**和从**内存中读取出变量**这样的底层细节。 + Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存(每个线程都分配有单独的处理器缓存,用这些处理器缓存去缓存一些数据,就可以不用再次访问主内存去获取相应的数据,这样就可以提高效率)。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。而JMM就作用于工作内存和主存之间数据同步过程。他规定了如何做数据同步以及什么时候做数据同步。对于共享普通变量来说,约定了变量在工作内存中发生变化了之后,必须要回写到工作内存(**迟早要回写但并非马上回写**),*但对于volatile变量则要求工作内存中发生变化之后,必须马上回写到工作内存,而线程读取volatile变量的时候,必须马上到工作内存中去取最新值而不是读取本地工作内存的副本*,此规则保证了前面所说的“当线程A对变量X进行了修改后,在线程A后面执行的其他线程能看到变量X的变动”。 @@ -96,8 +98,6 @@ Java内存模型规定了所有的变量都存储在主内存中,每条线程 - - ## volatile @@ -281,9 +281,12 @@ Monitor其实是一种同步工具,也可以说是一种同步机制,它通 + + 参考: - [再有人问你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) diff --git "a/JavaKnowledge/\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/\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" deleted file mode 100644 index 52e55a8e..00000000 --- "a/JavaKnowledge/\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" +++ /dev/null @@ -1,74 +0,0 @@ -原子性、可见性以及有序性 -=== - -- 原子性: - 众所周知,原子是构成物质的基本单位,所以原子代表着不可分。 - 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。 - 最简单的一个例子就是银行转账问题,赋值或者`return`。比如`a = 1;`和 `return a;`这样的操作都具有原子性 - 原子性不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作! - 加锁可以保证复合语句的原子性,`sychronized`可以保证多条语句在`synchronized`块中语意上是原子的。 - -- 可见性: - 在多核处理器中,如果多个线程对一个变量进行操作,但是这多个线程有可能被分配到多个处理器中运行,那么编译器会对代码进行优化,当线程要处理该变量时, - 多个处理器会将变量从主内存复制一份分别存储在自己的片上存储器中,等到进行完操作后,再赋值回主存。 - (这样做的好处是提高了运行的速度,因为在处理过程中多个处理器减少了同主内存通信的次数);同样在单核处理器中这样由于备份造成的问题同样存在! - 这样的优化带来的问题之一是变量可见性——如果线程`t1`与线程`t2`分别被安排在了不同的处理器上面,那么`t1`与`t2`对于变量`A`的修改时相互不可见,如果`t1`给`A`赋值,然后`t2`又赋新值,那么`t2`的操作就将`t1`的操作覆盖掉了, - 这样会产生不可预料的结果。所以,即使有些操作时原子性的,但是如果不具有可见性,那么多个处理器中备份的存在就会使原子性失去意义。 - -- 非原子性操作 - 类似`a += b`这样的操作不具有原子性,在某些`JVM`中`a += b`可能要经过这样三个步骤: - - - 取出`a`和`b` - - 计算`a+b` - - 将计算结果写入内存 - 如果有两个线程`t1`,`t2`在进行这样的操作。`t1`在第二步做完之后还没来得及把数据写回内存就被线程调度器中断了,于是`t2`开始执行,`t2`执行完毕后`t1`又把没有完成的第三步做完。这个时候就出现了错误, - 相当于`t2`的计算结果被无视掉了。所以上面的买碘片例子在同步`add`方法之前,实际结果总是小于预期结果的,因为很多操作都被无视掉了。 - 类似的,像`a++`这样的操作也都不具有原子性。所以在多线程的环境下一定要记得进行同步操作。 - -- 有序性 - 有序性:即程序执行的顺序按照代码的先后顺序执行。 - ```java - int i = 0; - boolean flag = false; - i = 1; //语句1 - flag = true; //语句2 - ``` - 上面代码定义了一个`int`型变量,定义了一个`boolean`类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么`JVM`在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗? - 不一定,为什么呢?这里可能会发生指令重排序`(Instruction Reorder)`。 -  下面解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。 -  比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。 -  但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢? - 再看下面一个例子: - ```java - int a = 10; //语句1 - int r = 2; //语句2 - a = a + 3; //语句3 - r = a*a; //语句4 - ``` - 这段代码有4个语句,那么可能的一个执行顺序是: - 语句2->语句1->语句3->语句4 - 那么可能不可能是这个执行顺序呢?语句2->语句1->语句4->语句3,这是不可能的,因为处理器在进行重排序时是会考虑指令之间的数据依赖性, - 如果一个指令`Instruction 2`必须用到`Instruction 1`的结果,那么处理器会保证`Instruction 1`会在`Instruction 2`之前执行。 - - 虽然重排序不会影响单个线程内程序执行的结果,但是多线程呢?下面看一个例子: - ```java - //线程1: - context = loadContext(); //语句1 - inited = true; //语句2 - - //线程2: - while(!inited ){ - sleep() - } - doSomethingwithconfig(context); - ``` - 上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此时线程2会以为初始化工作已经完成, - 那么就会跳出`while`循环,去执行`doSomethingwithconfig(context)`方法,而此时`context`并没有被初始化,就会导致程序出错。 - 从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。 -  也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。 - ---- -- 邮箱 :charon.chui@gmail.com -- Good Luck! - - From 07917cf6a0783cc7f8c3d290868dc8d9e1142c31 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 14 Jul 2020 17:47:05 +0800 Subject: [PATCH 028/213] add images --- ...14Synchronized\345\214\272\345\210\253.md" | 4 +- ...01\350\231\232\345\274\225\347\224\250.md" | 91 ++++++++++++++----- README.md | 4 +- 3 files changed, 70 insertions(+), 29 deletions(-) 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 d447a6d3..9dd6ce0a 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" @@ -109,9 +109,7 @@ Java内存模型规定了所有的变量都存储在主内存中,每条线程 -- - -volatile如何实现内存可见性。深入来说:通过加入**内存屏障**和**禁止重排序优化**来实现的 +- volatile如何实现内存可见性。深入来说:通过加入**内存屏障**和**禁止重排序优化**来实现的 - 对volatile变量执行写操作时,会在写操作后加入一条store屏障指令 - 对volatile变量执行读操作时,会在读操作前加入一条load屏障指令 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 9f7da395..1f4346c1 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" @@ -1,37 +1,80 @@ 强引用、软引用、弱引用、虚引用 === +在JDK1.2以前的版本中,当一个对象不被任何变量引用,那么程序就无法再使用这个对象。也就是说,只有对象处于可触及状态,程序才能使用它。这就像在日常生活中,从商店购买了某样物品后,如果有用,就一直保留它,否则就把它扔到垃圾箱,由清洁工人收走。一般说来,如果物品已经被扔到垃圾箱,想再把它捡回来使用就不可能了。 +但有时候情况并不这么简单,你可能会遇到类似鸡肋一样的物品,食之无味,弃之可惜。这种物品现在已经无用了,保留它会占空间,但是立刻扔掉它也不划算,因为也许将来还会派用场。对于这样的可有可无的物品,一种折衷的处理办法是:如果家里空间足够,就先把它保留在家里,如果家里空间不够,即使把家里所有的垃圾清除,还是无法容纳那些必不可少的生活用品,那么再扔掉这些可有可无的物品。 +从JDK1.2版本开始,把对象的引用分为四种级别,从而使程序能更加灵活的控制对象的生命周期。 + +***这四种级别由高到低依次为:强引用 > 软引用 > 弱引用 > 虚引用。*** + + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/reference_list.jpg) + +在java.lang.ref包中提供了三个类:SoftReference类、WeakReference类和PhantomReference类,它们分别代表软引用、弱引用和虚引用。ReferenceQueue类表示引用队列,它可以和这三种引用类联合使用,以便跟踪Java虚拟机回收所引用的对 象的活动。 + + + - 强引用(Strong Reference) - 平时我们编程的时候例如:`Object object=new Object()`;那`object`就是一个强引用了。如果一个对象具有强引用,那就类似于必不可少的生活用品, - 垃圾回收器绝不会回收它。当内存空 间不足,`Java`虚拟机宁愿抛出`OutOfMemoryError`错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。 + + 你懂的,不要胡乱持有着不放,不然内存泄露、oom有你好看,就像是老板(OOM)的亲儿子一样,在公司可以什么事都不干,但是千万不要老是占用公司的资源为他自己做事,记得用完公司的妹子之后,要让她们去工作(资源要懂得释放) 不然公司很可能会垮掉的。 + 平时我们编程的时候例如:`Object object=new Object()`;那`object`就是一个强引用了。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当JVM的内存空间不足时,宁愿抛出OutOfMemoryError使得程序异常终止也不愿意回收具有强引用的存活着的对象!记住是存活着,不可能是你new一个对象就永远不会被GC回收。 + - 软引用(SoftReference) - 如果一个对象只具有软引用,那就类似于可有可物的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。 - 只要垃圾回收器没有回收它,该对象就可以被程序使用。**软引用可用来实现内存敏感的高速缓存**。 软引用可以和一个引用队列`(ReferenceQueue)`联合使用, + + 描述一些还有用,但并非必需的对象。如果一个对象只具有软引用,那就类似于可有可物的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存,但是system.gc对其无效,有点像老板(OOM)的亲戚,在公司表现不好有可能会被开除,即使你投诉他(调用GC)上班看片,但是只要不被老板看到(被JVM检测到)就不会被开除(被虚拟机回收)。 + +**软引用可用来实现内存敏感的高速缓存**。 软引用可以和一个引用队列`(ReferenceQueue)`联合使用, 如果软引用所引用的对象被垃圾回收,`Java`虚拟机就会把这个软引用加入到与之关联的引用队列中。 - + - 弱引用(WeakReference) - 弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。 - 弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器 - 弱引用可以和一个引用队列`(ReferenceQueue)`联合使用,如果弱引用所引用的对象被垃圾回收,`Java`虚拟机就会把这个弱引用加入到与之关联的引用队列中。 - + + 同软引用,也用来描述非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。在对象没有其他引用的情况下,调用system.gc对象可被虚拟机回收,就是一个普通的员工,平常如果表现不佳会被开除(对象没有其他引用的情况下),遇到别人投诉(调用GC)上班看片,那开除是肯定了(被虚拟机回收)。 + + 在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了***只具***有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。弱引用最常见的用途是实现规范映射(canonicalizing mappings,比如哈希表)。 + 常见的一个例子就是WeakHashMap,在HashMap中,键被置为null,唤醒gc后,不会垃圾回收键为null的键值对。但是在WeakHashMap中,键被置为null,唤醒gc后,键为null的键值对会被回收。 + + ``` + public static void weakHashMapTest() { + Integer key = new Integer(1); + String value = "李四"; + Map weakHashMap = new WeakHashMap(); + weakHashMap.put(key, value); + System.out.println(weakHashMap);//{1=李四} + key = null; + System.gc(); + System.out.println(weakHashMap);//{} + } + public static void hashMapTest() { + HashMap map = new HashMap<>(); + Integer key = 1; + String value = "张三"; + map.put(key,value); + System.out.println(map);//{1=张三} + key = null; + System.gc(); + System.out.println(map);//{1=张三} + } + ``` + - 虚引用(PhantomReference) - "虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用, - 那么它就和没有任何引用一样,在 任何时候都可能被垃圾回收。 虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于: - 虚引用必须和引用队列 `(ReferenceQueue)`联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前, - 把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。 - 程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。 虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引用。 - 虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引用。 - 虚引用主要用于检测对象是否已经从内存中删除。 - - -有关弱引用以及软引用再分析一下: -我们都知道垃圾回收器会回收符合回收条件的对象的内存,但并不是所有的程序员都知道回收条件取决于指向该对象的引用类型。这正是`Java`中弱引用和软引用的主要区别。 -如果一个对象只有弱引用指向它,垃圾回收器会立即回收该对象,这是一种急切回收方式。相对的,如果有软引用指向这些对象,则只有在`JVM`需要内存时才回收这些对象。 -弱引用和软引用的特殊行为使得它们在某些情况下非常有用。例如:软引用可以很好的用来实现缓存,当`JVM`需要内存时,垃圾回收器就会回收这些只有被软引用指向的对象。 -而弱引用非常适合存储元数据,例如:存储`ClassLoader`引用。如果没有类被加载,那么也没有指向`ClassLoader`的引用。一旦上一次的强引用被去除, -只有弱引用的`ClassLoader`就会被回收。 + + "虚引用"顾名思义,就是形同虚设,也成为幽灵引用或幻影引用,它是最弱的一种引用关系。就只是一个标识,对象的生命周期不受期影响,这货估计就是个临时工把,遇到事情的时候想到了你,没有事情的时候,秒秒钟拿出去顶锅,开除。 + + 一个对象是否有虚引用的存在完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一的用处:能在对象被GC时收到系统通知,主要用于跟踪对象何时被回收,比如防止资源泄漏等。 + + 虚引用必须和引用队列 `(ReferenceQueue)`联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。 虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null。 + + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/reference_compare.jpg) + + + + + --- diff --git a/README.md b/README.md index 766477dd..316ffbc9 100644 --- a/README.md +++ b/README.md @@ -202,7 +202,7 @@ Android学习笔记 - [常见算法][95] - [网络请求相关内容总结][96] - [线程池的原理][97] - - [原子性、可见性以及有序性][98] + - [Java并发编程之原子性、可见性以及有序性][98] - [Base64加密][99] - [Git简介][100] - [hashCode与equals][101] @@ -387,7 +387,7 @@ Android学习笔记 [95]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/%E5%B8%B8%E8%A7%81%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/%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 "原子性、可见性以及有序性" +[98]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/Java并发编程之原子性、可见性以及有序性.md "Java并发编程之原子性、可见性以及有序性" [99]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/Base64%E5%8A%A0%E5%AF%86.md "Base64加密" [100]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/Git%E7%AE%80%E4%BB%8B.md "Git简介" [101]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/hashCode%E4%B8%8Eequals.md "hashCode与equals" From e9ff7c5e8cad6d9753f9efaaed909ffb97c3ce2d Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 15 Jul 2020 15:40:30 +0800 Subject: [PATCH 029/213] update --- .../MVC\344\270\216MVP\345\217\212MVVM.md" | 8 ++ ...04\345\244\215\346\235\202\345\272\246.md" | 27 +++--- ...13\346\261\240\347\256\200\344\273\213.md" | 84 ++++++++++++++++--- 3 files changed, 92 insertions(+), 27 deletions(-) diff --git "a/JavaKnowledge/MVC\344\270\216MVP\345\217\212MVVM.md" "b/JavaKnowledge/MVC\344\270\216MVP\345\217\212MVVM.md" index 031a3198..428ac290 100644 --- "a/JavaKnowledge/MVC\344\270\216MVP\345\217\212MVVM.md" +++ "b/JavaKnowledge/MVC\344\270\216MVP\345\217\212MVVM.md" @@ -64,6 +64,14 @@ MVVM 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很好的解决了MVC和MVP的不足,但是由于数据和视图的双向绑定,导致出现问题时不太好定位来源,有可能数据问题导致,也有可能业务逻辑中对视图属性的修改导致。如果项目中打算用MVVM的话可以考虑使用官方的架构组件ViewModel、LiveData、DataBinding去实现MVVM + + + - 优点 `MVVM`模式和`MVC`模式一样,主要目的是分离视图`View`和模型`Model` - 低耦合。 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" index 1465cb7b..341bdff2 100644 --- "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" @@ -41,15 +41,16 @@ 什么是时间复杂度,算法中某个函数有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`的某个函数。 @@ -139,26 +140,24 @@ O(2的`n`次方) 比如求具有`n`个元素集合的所有子集的算法 空间复杂度 --- -算法的空间复杂度并不是计算实际占用的空间,而是计算整个算法的辅助空间单元的个数,与问题的规模没有关系。算法的空间复杂度`S(n)`定义为该算法所耗费空间的数量级。 -`S(n)=O(f(n))`若算法执行时所需要的辅助空间相对于输入数据量`n`而言是一个常数,则称这个算法的辅助空间为`O(1)`; -递归算法的空间复杂度:递归深度`N*`每次递归所要的辅助空间, 如果每次递归所需的辅助空间是常数,则递归的空间复杂度是`O(N)`. +算法的空间复杂度并不是计算实际占用的空间,而是计算整个算法的辅助空间单元的个数,与问题的规模没有关系。算法的空间复杂度`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)`。若形参为数组,则只需要为它分配一个存储由实参传送来的一个地址指针的空间, -  即一个机器字长空间;若形参为引用方式,则也只需要为其分配存储一个地址的空间,用它来存储对应实参变量的地址, -  以便由系统自动引用实参变量。 +  算法的空间复杂度一般也以数量级的形式给出。如当一个算法的空间复杂度为一个常量,即不随被处理数据量`n`的大小而改变时,可表示为`O(1)`; +  当一个算法的空间复杂度与以2为底的`n`的对数成正比时,可表示为`O(log2n)`; +  当一个算法的空间复杂度与`n`成线性比例关系时,可表示为`O(n)`。若形参为数组,则只需要为它分配一个存储由实参传送来的一个地址指针的空间,即一个机器字长空间;若形参为引用方式,则也只需要为其分配存储一个地址的空间,用它来存储对应实参变量的地址,以便由系统自动引用实参变量。 -空间复杂度补充: +空间复杂度补充: -一个程序的空间复杂度是指运行完一个程序所需内存的大小。利用程序的空间复杂度,可以对程序的运行所需要的内存多少有个预先估计。 -一个程序执行时除了需要存储空间和存储本身所使用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为现实计算所需信息的辅助空间。 +一个程序的空间复杂度是指运行完一个程序所需内存的大小。利用程序的空间复杂度,可以对程序的运行所需要的内存多少有个预先估计。 +一个程序执行时除了需要存储空间和存储本身所使用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为现实计算所需信息的辅助空间。 程序执行时所需存储空间包括以下两部分:    - 固定部分。这部分空间的大小与输入/输出的数据的个数多少、数值无关。主要包括指令空间(即代码空间)、数据空间(常量、简单变量)等所占的空间。这部分属于静态空间。 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 ac933943..1ec4f548 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" @@ -19,18 +19,18 @@ - 任务接口(`Task`):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等 - 任务队列(`TaskQueue`):用于存放没有处理的任务。提供一种缓冲机制。 - - 使用线程池的好处: - - 减少在创建和销毁线程上所花的时间以及系统资源的开销 - - 如不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内存以及”过度切换”。 - - 提交相应速度 + - 降低资源消耗。通过重复利用减少在创建和销毁线程上所花的时间以及系统资源的开销 + - 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源, + 还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。如不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内存以及”过度切换”。 + - 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。 - 工作流程 线程池的任务就在于负责这些线程的创建,销毁和任务处理参数传递、唤醒和等待。 - 创建若干线程,置入线程池 - 任务达到时,从线程池取空闲线程 - 取得了空闲线程,立即进行任务处理 - - 否则新建一个线程,并置入线程池,执行3 + - 否则新建一个线程,并置入线程池,并执行上一步 - 如果创建失败或者线程池已满,根据设计策略选择返回错误或将任务置入处理队列,等待处理 - 销毁线程池 @@ -76,10 +76,10 @@ public class ThreadPoolExecutor extends AbstractExecutorService { 上面构造器中各参数的含义: -- `corePoolSize`:核心池的大小。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务, +- `corePoolSize`:用来表示线程池中的核心线程的数量,也可以称为可闲置的线程数量。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务, 除非调用了`prestartAllCoreThreads()`或者`prestartCoreThread()`方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建`corePoolSize`个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到`corePoolSize`后,就会把到达的任务放到缓存队列当中; -- `maximumPoolSize`:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程; +- `maximumPoolSize`:来表示线程池中最多能够创建的线程数量。这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程; - `keepAliveTime`:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于`corePoolSize`时,`keepAliveTime`才会起作用,直到线程池中的线程数不大于`corePoolSize`,即当线程池中的线程数大于`corePoolSize`时,如果一个线程空闲的时间达到`keepAliveTime`,则会终止,直到线程池中的线程数不超过`corePoolSize`。但是如果调用了`allowCoreThreadTimeOut(boolean)`方法,在线程池中的线程数不大于`corePoolSize`时,`keepAliveTime`参数也会起作用,直到线程池中的线程数为0; - `unit`:参数`keepAliveTime`的时间单位,有7种取值,在`TimeUnit`类中有7种静态属性: @@ -100,7 +100,7 @@ public class ThreadPoolExecutor extends AbstractExecutorService { - `threadFactory`:线程工厂,主要用来创建线程 - `handler`:表示当拒绝处理任务时的策略,有以下四种取值: - + - `ThreadPoolExecutor.AbortPolicy`:丢弃任务并抛出`RejectedExecutionException`异常 - `ThreadPoolExecutor.DiscardPolicy`:也是丢弃任务,但是不抛出异常 - `ThreadPoolExecutor.DiscardOldestPolicy`:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程) @@ -111,15 +111,18 @@ public class ThreadPoolExecutor extends AbstractExecutorService { 线程池状态 --- +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/thread_poll_state.png) + + + `ThreadPoolExecutor`中定义了一个`volatile`变量,另外定义了几个`static final`变量表示线程池的各个状态: -- `volatile int runState;` -- `static final int RUNNING = -1;`当创建线程池后,初始时,线程池处于`RUNNING`状态; +- `volatile int runState;` 表示当前线程池的状态,它是一个`volatile`变量用来保证线程之间的可见性 +- `static final int RUNNING = -1;`此状态下,线程池可以接受新的任务,也可以处理阻塞队列中的任务。执行shutdown()方法可进入待关闭(SHUTDOWN)状态,执行shutdownNow()方法可进入停止(STOP)状态。 - `static final int SHUTDOWN = 0;`如果调用了`shutdown()`方法,则线程池处于`SHUTDOWN`状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕 - `static final int STOP = 1;`如果调用了`shutdownNow()`方法,则线程池处于`STOP`状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务; -- `static final int TIDYING = 2;`该状态表示线程池对线程进行整理优化; -- `static final int TERMINATED = 3;`当线程池处于`SHUTDOWN`或`STOP`状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为`TERMINATED`状态 -`runState`表示当前线程池的状态,它是一个`volatile`变量用来保证线程之间的可见性; +- `static final int TIDYING = 2;`此状态下,所有任务都已经执行完毕,且没有工作线程。执行terminated()方法进入终止(TERMINATED)状态。该状态表示线程池对线程进行整理优化; +- `static final int TERMINATED = 3;`当线程池处于`SHUTDOWN`或`STOP`状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为`TERMINATED`状态。此状态下,线程池完全终止,并完成了所有资源的释放。 `Executors`类 @@ -170,6 +173,61 @@ public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 实际中,如果`Executors`提供的三个静态方法能满足要求,就尽量使用它提供的三个方法,因为自己去手动配置`ThreadPoolExecutor`的参数有点麻烦,要根据实际任务的类型和数量来进行配置。 + +## 线程池执行状态 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/thread_poll_process.png) + +整个过程可以拆分成以下几个部分: + +- 提交任务 + + 当向线程池提交一个新的任务时,线程池有三种处理情况,分别是:创建一个工作线程来执行该任务、将任务加入阻塞队列、拒绝该任务。 + + 提交任务的过程也可以拆分成以下几个部分: + + 1. 当工作线程数小于核心线程数时,直接创建新的核心工作线程。 + 2. 当工作线程数大于核心线程数时,就需要尝试将任务添加到阻塞队列中去。 + 3. 如果能够加入成功,说明队列还没满,那么就需要做以下的二次校验来保证添加进去的任务能够成功被执行。 + 4. 验证当前线程池中的运行状态,如果是非RUNNING状态,则需要将任务从阻塞队列中移除,然后拒绝该任务。 + 5. 验证当前线程池中的工作线程的个数,如果是0,则需要主动添加一个空工作线程来执行刚刚添加到阻塞队列中的任务。 + 6. 如果加入失败,说明队列已经满了,这时就需要创建新的临时工作线程来执行任务。 + 7. 如果创建成功,则直接执行该任务。 + 8. 如果创建失败,说明工作线程数已经等于最大线程数了,只能拒绝该任务了。 + +- 创建工作线程 + + 创建工作线程需要做一系列的判断,需要确保当前线程池可以创建新的线程之后,才能创建。 + + 首先,当线程池的状态是SHUTDOWN或者STOP时,不能创建新的线程。 + + 其次,当线程工厂创建线程失败时,也不能创建新的线程。 + + 第三,拿当前工作线程的数量与核心线程数、最大线程数进行比较,如果前者大于后者的话,也不允许创建。除此之外,线程池会尝试通过CAS来自增工作线程的个数,如果自增成功了,则会创建新的工作线程,即Worker对象。 + + 然后加锁进行二次验证是否能够创建工作线程,如果最后创建成功,则会启动该工作线程。 + +- 启动工作线程 + + 当工作线程创建成功后,也就是Worker对象已经创建好了,这时就需要启动该工作线程,让线程开始干活了,Worker对象中关联着一个Thread,所以要启动工作线程的话,只要通过worker.thread.start()来启动该线程即可。 + + 启动完了之后,就会执行Worker对象的run方法,因为Worker实现了Runnable接口,所以本质上Worker也是一个线程。 + + 通过线程start开启之后就会调用到Runnable的run方法,在Worker对象的run方法中,调用了runWorker(this)方法,也就是把当前对象传递给了runWorker()方法,让它来执行。 + +- 获取任务并执行 + + 在runWorker方法被调用之后,就是执行具体的任务了,首先需要拿到一个可以执行的任务,而Worker对象中默认绑定了一个任务,如果该任务不为空的话,那么就是直接执行。 + + 执行完了之后,就会去阻塞队列中获取任务来执行。 + + 获取任务的过程则需要考虑当前工作线程的个数: + + 1. 如果工作线程数大于核心线程数,那么就需要通过poll(keepAliveTime, timeUnit)来获取,因为这时需要对闲置线程进行超时回收。 + 2. 如果工作线程数小于等于核心线程数,那么就可以通过take()来获取了。因为这时所有的线程都是核心线程,不需要进行回收,前提是没有设置allowCoreThreadTimeOut(允许核心线程超时回收)为true。 + + + 向线程池提交任务 --- From 53cb30b26a01160e5e6626393344b455364b86a6 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 23 Jul 2020 19:38:48 +0800 Subject: [PATCH 030/213] update images --- .../1. ExoPlayer\347\256\200\344\273\213.md" | 2 +- ...47\350\203\275\344\274\230\345\214\226.md" | 76 ++++++++++++++++--- 2 files changed, 67 insertions(+), 11 deletions(-) diff --git "a/VideoDevelopment/ExoPlayer/1. ExoPlayer\347\256\200\344\273\213.md" "b/VideoDevelopment/ExoPlayer/1. ExoPlayer\347\256\200\344\273\213.md" index d3355a06..09afc156 100644 --- "a/VideoDevelopment/ExoPlayer/1. ExoPlayer\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/ExoPlayer/1. ExoPlayer\347\256\200\344\273\213.md" @@ -37,7 +37,7 @@ implementation 'com.google.android.exoplayer:exoplayer-ui:2.X.X' - `exoplayer-smoothstreaming`:支持SmoothStreaming - `exoplayer-ui`:使用ExoPlayer所需的UI部分和资源 -上面讲到了DASH、HLS、SmoothStreaming,具体的介绍可以参考[流媒体通信协议]() +上面讲到了DASH、HLS、SmoothStreaming,具体的介绍可以参考[流媒体通信协议](https://github.com/CharonChui/AndroidNote/tree/master/VideoDevelopment/%E6%B5%81%E5%AA%92%E4%BD%93%E5%8D%8F%E8%AE%AE) 除了上面的几个类库外,ExoPlayer还有很多扩展库提供一些额外的功能,有一些可以直接通过JCenter进行依赖,有一些需要手动去编译,具体的可以通过[扩展库目录](https://github.com/google/ExoPlayer/tree/release-v2/extensions/)中的README来查看详细的内容。 可以通过JCenter进行依赖的类库和扩展可以从[ExoPlayer的Binatry](https://bintray.com/google/exoplayer)上查看。 diff --git "a/VideoDevelopment/\346\222\255\346\224\276\345\231\250\346\200\247\350\203\275\344\274\230\345\214\226.md" "b/VideoDevelopment/\346\222\255\346\224\276\345\231\250\346\200\247\350\203\275\344\274\230\345\214\226.md" index ba32b52d..d1c96ecb 100644 --- "a/VideoDevelopment/\346\222\255\346\224\276\345\231\250\346\200\247\350\203\275\344\274\230\345\214\226.md" +++ "b/VideoDevelopment/\346\222\255\346\224\276\345\231\250\346\200\247\350\203\275\344\274\230\345\214\226.md" @@ -10,10 +10,10 @@ - ### 成本优化 #### 预下载控制 + - 不限速 - 非高峰时段,预下载N秒 - 高峰时段,预下载M秒 @@ -29,11 +29,43 @@ 对解码能力进行评估,来通过机型打分控制是否使用H265以及是用360p还是480p。 但是H265全部切换成本较高,牵扯到转码成本以及存储成本,所以可以只对热点的视频去进行编码,这样少量的视频就可以带来可观的效果。 +#### 防盗链 + + +会有一个防盗链的请求,通过 HTTP 拿到真实的播放 URL 才可以下载数据。 + +但是在手机上执行 HTTP 请求非常耗时,这里走私有长连接通道做这个事情。 + +#### PCDN + +有关CDN相关可参考[CDN及PCDN](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/CDN%E5%8F%8APCDN.md) + ### 秒开 -能立即起播,不会产生缓冲 +能立即起播,不会产生缓冲。秒开很简单,预加载数据就行。不够快就预解封装、解码,还不够快就再加上预渲染。但是秒开又是最难的。 播放器初始化->业务逻辑->DNS解析->CDN下载数据->解码->渲染播放 +“在所有环节中,短视频列表播放的体验非常重要。例如抖音,你会发现起播非常快,循环播放也很流畅。这是怎么做到的呢?这就是通过列表预加载技术实现的。常规的预加载是通过多个播放器来实现的,播放当前视频的时候去预加载下一个视频,这个方案的缺点是实现逻辑非常复杂,同时也消耗更多性能。所以,阿里云独创了列表播放器,通过简单的接口调用就可以实现列表的预加载播放。它有几个特点,首先是能够做到防卡顿的缓存策略,通过对缓存的管理,可以灵活控制卡顿期间的预缓存策略,同时优化缓冲的淘汰策略。第二是对滑动的流畅性针对性优化,保证每个视频停止的耗时在16毫秒以内。第三是采用了基于内存的预加载缓存技术,循环播放和秒开直接从内存读取数据,无需额外的文件操作。第四非常关键,是提供简单的接口,可以非常快速的实现短视频播放功能 + + + +#### 预加载 + +预加载能减少首次缓冲时间,但是预加载不能和当前播放的视频抢下载带宽,需要达到一个合适的阈值再开始下载。 + +优先保证封面图等信息完成后再根据云控的当前缓冲的时长等信息来控制是否要启动预加载 + +提升视频加载速度的常见手段就是预加载 ,即在用户播放之前,预先下载好一部分数据,这样在视频开播的时候就不需要等待网络请求。这里面有两个挑战,一个是预加载需要下载多少数据才算够呢?下载多了,浪费带宽;下载少了,不够拿来开播,也就无法满足快开的要求。拿 MP4来说,要解码出首帧,首先需要解析完 moov (可以理解为 header),而 moov的大小则和视频的长度相关。所以需要针对不同的视频计算出不同的预加载大小。第二个是即便视频数据已经预先下载完毕,但在开播前播放器的创建,外壳 view的布局,内核中的 MP4 解封装、解码、渲染仍然会导致有数百毫秒级别的耗时,在全屏沉浸式的场景中仍然能被用户感知到加载过程。 +为了实现极致的快开体验,我们实现了多实例的播放器,在预加载的基础上更极致地缩减开播耗时,实现了直出的开播体验。一般情况下,卡顿和视频清晰度是互斥的,清晰度越高,视频码率越高,用户越容易遇到缓冲卡顿。为了一个指标牺牲另一个指标是不被接受的。我们的视频内容在下发前会在云端预先转码成多档清晰度,并在端的播放器实现了一套在播放过程中基于用户的网络类型、平均网速、视频时长等指标动态升级或降级清晰度的策略,保证了绝大多数用户都能观看到匹配自己网络条件、最高清晰度的视频,在提升整体视频质量的同时守住了卡顿率。 + +- 预加载时机提前 + + 利用滑动切换视频时,在滑动松手到滑动停止这段时间(300 毫秒),在保证滑动帧率的前提下,开启子线程预加载后面的视频,利用这 300 毫秒,预加载后面的视频流,保证本地起播。相比原先预加载时机是在滑动停止,当前视频起播后才预加载后面的视频,假设视频起播时间为 500 毫秒,则相比优化前,预加载时机提前了至少 800 毫秒,这是非常可观的提升。最后测试下来,也是此优化项对于缓存命中率的提升是最为明显的。 + +- 多播放器 + + 空间换时间。双播放器方案原理比较简单,就是在当前视频起播之后,预准备下一个视频的播放器,两个播放器循环使用。下一个视频的播放器一旦准备好,加上视频流已加载到本地,则起播非常快,几乎是视频直出的效果。但由于播放器准备更耗时,用户滑到下一个视频时,并不能百分百保证此视频的播放器已准备好。 + #### MOOV后置 很多手机为了偷懒,录制完视频后才知道视频信息,所以把moov放到末尾。对于moov放到视频最后的情况下,就要多一次seek,当从头开始解析的时候如果发现未找到moov信息,需要将其seek到尾部再去寻找,这样会导致起播慢。 @@ -55,22 +87,46 @@ IP竞速 DNS解析加快,通常,DNS解析,意味着要把一个域名为xxx.com解析成ip过程,平时请求网页,网络差,就会打开网页半天。 -#### 防盗链 -会有一个防盗链的请求,通过 HTTP 拿到真实的播放 URL 才可以下载数据。 +### 监控 + +监控上报,肯定是不可缺少的,这是一个成熟的项目必备的要素。 + +1. 问题定位,老板跟用户反馈说我这个视频播不了,要有一套成熟的问题定位的方式; + 传统的捞Log方式大家都有,但是这种方式效率太低,需要等用户上线之后才能捞到Log,Log捞到之后还得花时间去分析。我们做法的是在关键问题上做一些插装,把每一类错误和每一个具体的子错误都能定义出来,这样一看错误码就知道播放错误是由什么原因导致的。还可以把每次播放视频的链路所有关键流水上报到统计系统里来,每一次播放都是一组流水,每一条流水里面就包含了例如首次缓冲发生的Seek,或下载的链接是多少,下载的时间是多少,有了这些流水之后,用户反馈播放失败,我首先可以用流水看发生了什么错误?错误在哪一步?每一步信息是什么?几秒钟就可以定位到问题。 + +有了这个数据上报之后,还可以做一些报表。比如说可以做错误码的报表,有了报表之后就可以跟进哪个错误是在TOP的,负责人是谁,原因是什么,都可以看到。 + +我们也有自己实时的曲线,可以看到各项数据的情况。在告警方面,基于成功率和失败率的统计,进行实时告警。 + + + + + + + + + + + + + + + + + + + + + + -但是在手机上执行 HTTP 请求非常耗时,这里走私有长连接通道做这个事情。 -#### 预加载 -预加载能减少首次缓冲时间,但是预加载不能和当前播放的视频抢下载带宽,需要达到一个合适的阈值再开始下载。 -优先保证封面图等信息完成后再根据云控的当前缓冲的时长等信息来控制是否要启动预加载 -#### PCDN -有关CDN相关可参考[CDN及PCDN](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/CDN%E5%8F%8APCDN.md) --- From 6839d71ea8f2840a9f2e614f69894e51da17cdfb Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 26 Nov 2020 09:22:08 +0800 Subject: [PATCH 031/213] add OperatingSystem --- ...66\346\236\204\347\256\200\344\273\213.md" | 37 +++ ...ps\347\232\204\345\214\272\345\210\253.md" | 39 +++ ...15\344\275\234\347\263\273\347\273\237.md" | 287 +++++++++++++++++ .../2.\350\277\233\347\250\213.md" | 303 ++++++++++++++++++ ...05\345\255\230\347\256\241\347\220\206.md" | 277 ++++++++++++++++ .../4.\350\260\203\345\272\246.md" | 87 +++++ OperatingSystem/5.IO.md | 42 +++ ...07\344\273\266\347\256\241\347\220\206.md" | 67 ++++ ...45\345\274\217\347\263\273\347\273\237.md" | 27 ++ ...8.\350\231\232\346\213\237\346\234\272.md" | 134 ++++++++ 10 files changed, 1300 insertions(+) create mode 100644 "Architect/1.\346\236\266\346\236\204\347\256\200\344\273\213.md" create mode 100644 "OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237.md" create mode 100644 "OperatingSystem/2.\350\277\233\347\250\213.md" create mode 100644 "OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" create mode 100644 "OperatingSystem/4.\350\260\203\345\272\246.md" create mode 100644 OperatingSystem/5.IO.md create mode 100644 "OperatingSystem/6.\346\226\207\344\273\266\347\256\241\347\220\206.md" create mode 100644 "OperatingSystem/7.\345\265\214\345\205\245\345\274\217\347\263\273\347\273\237.md" create mode 100644 "OperatingSystem/8.\350\231\232\346\213\237\346\234\272.md" diff --git "a/Architect/1.\346\236\266\346\236\204\347\256\200\344\273\213.md" "b/Architect/1.\346\236\266\346\236\204\347\256\200\344\273\213.md" new file mode 100644 index 00000000..f75f78d3 --- /dev/null +++ "b/Architect/1.\346\236\266\346\236\204\347\256\200\344\273\213.md" @@ -0,0 +1,37 @@ +1.系统架构 +=== + +### 架构三要素 + +#### 构件 + +构件在软件领域是指可复用的模块,它可以是被封装的对象类、类树、一些功能模块、软件框架(framework)、软件架构(或体系结构Architectural)、文档、分析件、设计模式(Pattern)。但是,操作集合、过程、函数即使可以复用也不能成为一个构件。 + +##### 构件的属性: +1. 有用性(Usefulness):构件必须提供有用的功能。 +2. 可用性(Usability):构件必须易于理解和使用,可以正常运行。 +3. 质量(Quality):构件及其变形必须能正确工作,质量好坏与可用性相互补充。 +4. 适应性(Adaptability):构件应该易于通过参数化等方式再不同环境中进行配置,比较高端一点的复用性,接收外界各种入参,产生不同的结果,健壮性比较高。 +5. 可移植性(Portability):构件应能在不同的硬件运行平台和软件环境中工作,可移植性比较好,跨平台。 + + +#### 模式(Pattern) + +其实就是解决某一类问题的方法论,是生产经验和生活经验中经过抽象和升华提炼出来的核心知识体系。 模式就是一个完整的流程闭环,能够解决一些问题的通用方法(比如资本运作、玩家不同的需求等),软件中的模式大多源于生活,是人类智慧的结晶。 + +#### 规划 + +规划是系统架构中最重要的组成部分,是个人或者组织制定的比较全民长远的发展计划,是对未来整体性、长期性、基本性问题的思考和考量。设计未来整套行动的方案。很早就有规划这个概念了,例如:国家的十一五规划等。当然软件开发也和生活紧密联系,一个大型的系统也需要良好的规划,规划可以说是基石,是系统架构的前提。 + + +系统架构虽然是软件系统的结构,行为,属性的高级抽象,但其根本就是在需求分析的基础行为下,制定技术框架,对需求的技术实现。 + + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/JavaKnowledge/Http\344\270\216Https\347\232\204\345\214\272\345\210\253.md" "b/JavaKnowledge/Http\344\270\216Https\347\232\204\345\214\272\345\210\253.md" index 90346bfb..417c7785 100644 --- "a/JavaKnowledge/Http\344\270\216Https\347\232\204\345\214\272\345\210\253.md" +++ "b/JavaKnowledge/Http\344\270\216Https\347\232\204\345\214\272\345\210\253.md" @@ -9,8 +9,14 @@ HTTP与HTTPS的区别 - 1997年发布HTTP/1.1: 持久连接(长连接)、节约带宽、HOST域、管道机制、分块传输编码。 + 长连接是指的TCP连接,也就是说复用的是TCP连接。即长连接情况下,多个HTTP请求可以复用同一个TCP连接,这就节省了很多TCP连接建立和断开的消耗。 + + 此外,长连接并不是永久连接的。如果一段时间内(具体的时间长短,是可以在header当中进行设置的,也就是所谓的超时时间),这个连接没有HTTP请求发出的话,那么这个长连接就会被断掉。 + - 2015年发布HTTP/2:多路复用、服务器推送、头信息压缩、二进制协议等。 + + ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/http1.1vs2.jpg) 多路复用:通过单一的HTTP/2连接请求发起多重的请求-响应消息,多个请求stream共享一个TCP连接,实现多留并行而不是依赖建立多个TCP连接。 @@ -27,6 +33,25 @@ HTTP与HTTPS的区别 - 传输速度快 + + +### HTTP请求响应过程 + +你是不是很好奇,当你在浏览器中输入网址后,到底发生了什么事情?你想要的内容是如何展现出来的?让我们通过一个例子来探讨一下,我们假设访问的 URL 地址为 `http://www.someSchool.edu/someDepartment/home.index`,当我们输入网址并点击回车时,浏览器内部会进行如下操作 + +- DNS服务器会首先进行域名的映射,找到访问`www.someSchool.edu`所在的地址,然后HTTP 客户端进程在 80 端口发起一个到服务器 `www.someSchool.edu` 的 TCP 连接(80 端口是 HTTP 的默认端口)。在客户和服务器进程中都会有一个`套接字`与其相连。 +- HTTP 客户端通过它的套接字向服务器发送一个 HTTP 请求报文。该报文中包含了路径 `someDepartment/home.index` 的资源,我们后面会详细讨论 HTTP 请求报文。 +- HTTP 服务器通过它的套接字接受该报文,进行请求的解析工作,并从其`存储器(RAM 或磁盘)`中检索出对象 [www.someSchool.edu/someDepartment/home.index,然后把检索出来的对象进行封装,封装到](http://www.someSchool.edu/someDepartment/home.index,然后把检索出来的对象进行封装,封装到) HTTP 响应报文中,并通过套接字向客户进行发送。 +- HTTP 服务器随即通知 TCP 断开 TCP 连接,实际上是需要等到客户接受完响应报文后才会断开 TCP 连接。 +- HTTP 客户端接受完响应报文后,TCP 连接会关闭。HTTP 客户端从响应中提取出报文中是一个 HTML 响应文件,并检查该 HTML 文件,然后循环检查报文中其他内部对象。 +- 检查完成后,HTTP 客户端会把对应的资源通过显示器呈现给用户。 + +至此,键入网址再按下回车的全过程就结束了。上述过程描述的是一种简单的`请求-响应`全过程,真实的请求-响应情况可能要比上面描述的过程复杂很多。 + + + + + ## HTTPS Https并非是应用层的一种新协议。只是http通信接口部分用SSL(安全套接字层)和TLS(安全传输层协议)代替而已。即添加了加密及认证机制的HTTP称为HTTPS(HTTP Secure). @@ -135,7 +160,21 @@ HTTP + 加密 + 认证 + 完整性保护 = HTTPS +### Https 请求慢的解决办法 + +1. 不通过DNS解析,直接访问IP + +2. 解决连接无法复用 + + http/1.0协议头里可以设置Connection:Keep-Alive或者Connection:Close,选择是否允许在一定时间内复用连接(时间可由服务器控制)。但是这对App端的请求成效不大,因为App端的请求比较分散且时间跨度相对较大。 + + 方案1.基于tcp的长连接 (主要) 移动端建立一条自己的长链接通道,通道的实现是基于tcp协议。基于tcp的socket编程技术难度相对复杂很多,而且需要自己定制协议。但信息的上报和推送变得更及时,请求量爆发的时间点还能减轻服务器压力(避免频繁创建和销毁连接) + + 方案2.http long-polling 客户端在初始状态发送一个polling请求到服务器,服务器并不会马上返回业务数据,而是等待有新的业务数据产生的时候再返回,所以链接会一直被保持。一但结束当前连接,马上又会发送一个新的polling请求,如此反复,保证一个连接被保持。 存在问题: 1)增加了服务器的压力 2)网络环境复杂场景下,需要考虑怎么重建健康的连接通道 3)polling的方式稳定性不好 4)polling的response可能被中间代理cache住 …… + + 方案3.http streaming 和long-polling不同的是,streaming方式通过再server response的头部增加“Transfer Encoding:chuncked”来告诉客户端后续还有新的数据到来 存在问题: 1)有些代理服务器会等待服务器的response结束之后才将结果推送给请求客户端。streaming不会结束response 2)业务数据无法按照请求分割 …… + 方案4.web socket 和传统的tcp socket相似,基于tcp协议,提供双向的数据通道。它的优势是提供了message的概念,比基于字节流的tcp socket使用更简单。技术较新,不是所有浏览器都提供了支持。 diff --git "a/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237.md" "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237.md" new file mode 100644 index 00000000..00696a2d --- /dev/null +++ "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237.md" @@ -0,0 +1,287 @@ + + +# 1.操作系统 + + + +操作系统(Operating System, 简称OS)是管理和控制计算机硬件与软件资源的计算机程序,是一种由引导程序(bootloader)启动并管理计算机中所有程序生命周期的系统程序。任何其他软件都必须在操作系统的支持下才能运行,操作系统能有效组织和管理系统中的各种软、硬件资源,合理组织计算机系统的工作流程并控制程序的执行,为用户提供一个良好的操作环境。 目前比较为人所知的操作系统有Microsoft的Windows系统、Apple的Mac及以Linux为内核的各种Linux发行版(Centos/Ubuntu等)。 现代计算机系统由一个或多个处理器、主存、打印机、键盘、鼠标、显示器、网络接口及各种输入\输出设备构成。 + +作用: 它可以帮我们管理计算机的各种资源,协助我们完成各种复杂繁琐的任务。 + + + +### 以现代标准而言,一个标准PC的操作系统应该提供以下功能 + + + +### 用户界面(User interface) + + + +普通用户操作电脑是需要用户界面的,没有用户界面的电脑对于普通用户来说就是灾难。你能想象家里的老人或者上了年纪的人用电脑却没有鼠标的场景吗?你能想象使用黑框框来做 PPT 吗?你能想象使用黑框框来浏览网页吗? +专业的 IT 工作者有时候会使用黑框框纯粹是工作需要,在有些场景下,黑框框比用户界面更有效率一些。而类似于服务器场景的开发工作基本上是没有用户界面的,当然一直强调的是专业场景,这个世界上能流畅使用黑框框的人占总人口的比例太少太少。 +用户界面这项伟大的发明诞生于施乐公司,经乔布斯和比尔盖茨商业化运作后得以让世人发现它的伟大之处。用户界面的发明就相当于简体汉字的出现一般,简体汉字的推行让中国的文盲率大幅降低,而用户界面的发明则大幅降低了计算机的使用难度,所以你很难想象现代的操作系统没有用户界面。 + + + +#### 进程管理(Processing management) + + + +先来解释一下什么是进程。进程是计算机进行资源管理以及调度的基本单位,是程序的执行实体。这种说法比较正统,或者你可以将一个进程看成是计算机正在进行的一项任务。计算机里面有很多各式各样功能的进程,那么多进程如何比较好的运行是个深奥的学问。 +你可以想象一下:你打开了微信、word 文档、音乐软件,你想一边跟同事进行交流文档的内容应该如何写,一边在 word 文档敲下你的构思,然后在这个过程中你还听着音乐。在这个过程中,计算机需要将微信的网络保持连接,持续收发微信的信息,随时保存你的 word 文档到磁盘上,解码音乐流并播放出来。这一系列程序运转如何能让你产生一个假象:你以为它们是在同时运行的,但其实它们在同一时刻只有一个在运行。这就是进程管理和调度。 + + + +#### 内存管理(Memory management) + +内存是计算机很重要的一个资源,因为程序只有被加载到内存中才可以运行,此外 CPU 所需要的指令与数据也都是来自内存的。内存的并不是无限制的,它受限于硬件和寻址位数。但是现代操作系统会让每一个进程都觉得自己在独占整个内存,这就是虚拟内存技术。值得注意的是,这里的虚拟内存与 swap 这种虚拟内存是不一样的,虽然两个都是成为虚拟内存,但是完全是两个不同方向的技术。 +进程的运行需要分配内存,内存分配的快慢都与内存管理方式有着巨大的影响。两个不同进程对应的内存区域是不能相互访问的,操作系统必须得提供这样的保证,否则很容易出问题,比如:运行着的 dota 游戏如果可以被另外一个进程访问它的内存区域的话,那就可以直接将内存区域中的某个数值进行修改,比如将游戏中的玩家生命值变为无限,这样对手怎么打都打不死自己的英雄。例如前段时间火热的吃鸡游戏,外挂软件可以让角色在决赛圈外进行锁血,这个就是游戏内存被修改的最好示例。当然这是通过比较专业的手段来绕过操作系统的限制,这也从另外一个方面来说明,其实现在的操作系统安全性也是有很大提升空间的。 +进程退出销毁时,内存的回收也是很重要的,否则很容易就会产生内存溢出,占着茅坑不拉屎,导致其他的进程都憋死。 + + + +#### 文件系统(File system) + + + +文件系统与用户的距离很近,每个人平常在使用计算机的时候或多或少都会留下一些数据,而这些数据通常会保留在磁盘里面。磁盘如果不进行格式化的话,普通人是没法使用的。磁盘里面其实就是一些布满磁性物质的盘片,在计算机的世界里数据是 0 和 1 组成的,那对应的在磁盘里面就是磁性的正负极,也就是说计算机的一个文本数据要保存到磁盘中,那就需要将文本数据的电气化信号 0 和 1 通过磁盘翻译为磁性正负极并保存起来。 +磁盘格式化的过程就是将文件系统架设到磁盘上,这样可以更好的管理磁盘的数据。你可以将磁盘未格式化之前的数据看做是一堆杂乱无章散落在地上的书,而文件系统就是一个有编排顺序的书架,格式化的过程就是将这堆书一本本按编排顺序放到书架上。这个比喻不太恰当,因为格化式操作通常来说会清掉数据,就相当于将书里面的字都清掉了,放到书架上的书里面都是空白页,所以格式化的时候请谨慎。 + + + +#### 网络通信(Networking) + + + +我们常见的网络通信场景有:微信聊天、浏览网页、玩网络游戏等,可以说现在的操作系统如果不能上网感觉就没了灵魂。网络通信是个很复杂的过程,连很多专业的程序猿都说不清楚当他上网时网页是如何显示出来的,更别说整个网络的拓扑结构了。 +整个网络通信其实是一套约定好的通信协议,很多人第一次听说协议时觉得很高级,其实没什么高级的,简单的说,类似于我们军训时当教官喊立正我们必须得做出相应动作一样,一个指令对应一个动作。由于网络太复杂了,某位哲人说过如果某样东西太复杂可以通过分层来解决,于是网络分了 5 层。有人也许会说是 7 层,那是 OSI 标准,在实际应用中一般都是 5 层。由于网络这块内容实在是太复杂,后面我会另开一个专栏进行讲解。 + + + +#### 设备管理 + + + +计算机上有很多设备,比如CPU、内存、网卡、声卡、显卡、硬盘等。那什么是设备管理? +在计算机中除了 CPU 和内存,对于其他一切输入输出设备的管理统称为设备管理。 +计算机中的设备分为输入和输出设备。以 CPU 为中心,凡是向 CPU 输送数据的设备统称为输入设备,例如鼠标、键盘、摄像头等;同样以 CPU 为中心,凡是从 CPU 获取数据的设备统称为输出设备,如显示器等。有些设备既是输入设备也是输入设备,比如网卡等。 +一个比较常见的场景是当我们将 U盘插进电脑的 USB 插孔时,电脑能实时识别出 U 盘设备,那计算机为啥能实时识别出这些设备呢?之后会有章节讨论一下这个话题。 + + + +## 计算机的组成 + + + +计算机由处理器、存储器和输入\输出部件组成,每类部件都有一个或多个模块。这些部件以某种方式互连,以实现计算机执行程序的主要功能。因此,计算机有4个主要的结构化部件: + +- 处理器(Processor):控制计算机的操作,执行数据处理功能。只有一个处理器时,它通常指中央处理器(CPU) +- 内存(Main memory):存储数据和程序。此类存储器通常是易失性的,即当计算机关机时,存储器的内容会丢失。相对于此的是磁盘存储器,当计算机关机时,它的内容不会丢失。内存通常也成为实存储器(real memory)或主存储器(primary memory)。 +- 输入\输出模块(I/O modules):在计算机和外部环境之间移动数据。外部环境由各种外部设备组成,包括辅助存储器设备(如硬盘)、通信设备和终端。 +- 系统总线(System bus)在处理器、内存和输入\输出模块间提供通信的设施。 + + + +处理器的一种功能是与存储器叫唤数据。为此,它通常使用两个内部寄存器: + +- 存储器地址寄存器(Memory Address Register, MAR),用于确定下一次读/写的存储器地址。 +- 存储器缓冲寄存器(Memory Buffer Register,MBR),存放要写入存储器的数据或从存储器中读取的数据。 + +同理,输入/输出地址存储器(I/O Address Register,简称I/O AR或I/O地址寄存器)用于确定一个特定的输入/输出设备,输入/输出缓冲寄存器(I/O Buffer Register,简称I/O BR或I/O缓冲寄存器)用于在输入/输出模块和处理器间交换数据。 + + + +内存模块由一组单元组成,这些单元由顺序编号的地址定义。每个单元包含一个二进制数,它可解释为一个指令或数据。输入/输出模块在外部设备与处理器和存储器之间传送数据。输入/输出模块包含内存缓冲区,用于临时保存数据,直到它们被发送出去。 + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/computer_cpu_memory_io.png?raw=true) + + + +### 计算机打开电源后的执行 + +在每台计算机上有一块双亲板(在政治因素影响到计算机产业之前,它们曾称为“母版”)。在双亲板上有一个称为基本输入输出系统(Basic Input Output System, BIOS)的程序。在BIOS内有底层I/O软件,包括读键盘、写屏幕、进行磁盘I/O以及其他过程。在计算机启动时,BIOS开始运行。它首先检查所安装的RAM数量、键盘和其他基本设备是否已安装并正常相应。接着,它开始扫描PCIe和PCI总线并找出连在上面的所有设备。即插即用设备也被记录下来。如果现有的设备和系统上一次启动时的设备不同,则新的设备将被配置。然后BIOS通过尝试存储在CMOS存储器中的设备清单决定启动设备。用户可以在系统刚启动之后进入一个BIOS配置程序,对设备清单进行修改。典型的,如果存在CD-ROM(有时是USB),则系统试图从中启动(之前重装系统就是这么操作的)。如果失败,系统将从硬盘启动。启动设备上的第一个扇区被读入内存并执行。这个扇区中包含一个对保存在启动扇区末尾的分区表检查的程序,以确定哪个分区是活动的。然后,从该分区读入第二个启动装载模块。来自活动分区的这个装载模块被读入操作系统,并启动之。 + +然后,操作系统询问BIOS,以获得配置信息。对于每种设备,系统检查对应的设备驱动程序是否存在。如果没有,系统要求用户插入含有该驱动程序的CD-ROM(由设备供应商提供)或者从网络上下载驱动程序。一旦有了全部的设备驱动程序,操作系统就将它们调入内核。然后初始化有关表格,创建需要的任何背景进程,并在每个终端上启动登陆程序或GUI。 + +1. X86 PC开机时CPU处于实模式,开机时会将cs=0xffff,ip=0x0000 +2. 寻址0xFFFF0(ROM BIOS营社区) +3. 检查RAM、键盘、显示器、磁盘、主板等硬件 +4. 将磁盘0磁道0扇区(操作系统引导扇区)读入0x7c00处 +5. 设置 cs=0x07c0,ip=0x0000开始执行 + + + +## CPU(Central Processing Unit) + +CPU 是计算机的大脑,它主要和内存进行交互,从内存中提取指令并执行它。CPU(中央处理器)是一块超大规模的集成电路Integrated Circuit),是信息处理、程序运行的最终执行单元。其功能主要是解释计算机指令以及处理计算机软件中的数据。从功能方面来看,CPU的内部主要由寄存器,控制器,运算器构成,各部分之间由电流信号相互连通。其中运算器负责算术运算和逻辑运算,控制器负责计算指令的解析,产生各种控制指令,寄存器组用来临时存放参加运算的数据和计算的中间结果。CPU计算结果最终需要写到内存中,内存的存取速度远低于CPU,为提升数据交换速率,CPU内部一般还集成了高速缓存(CACHE),其中缓存分为一级缓存和二级缓存,一级缓存和CPU速率相当,二级缓存次之。其实在现在一些CPU中,一级缓存也会分为一级数据缓存和一级指令缓存。 + +由于访问内存获取执行或数据要比执行指令花费的时间长,因此所有的 CPU 内部都会包含一些`寄存器`来保存关键变量和临时结果。因此,在指令集中通常会有一些指令用于把关键字从内存中加载到寄存器中,以及把关键字从寄存器存入到内存中。 + + + +**程序是把寄存器作为对象来描述的** + +使用高级语言编写的程序会在编译后转化成机器语言,然后通过CPU内部的寄存器来处理。不同类型的CPU,其内部寄存器的数量,种类以及寄存器存储的数值范围都是不同的。根据功能的不同,我们可以将寄存器大致划分为八类。 + +**累加寄存器:**存储执行运算的数据和运算后的数据。 + +**标志寄存器:**存储运算处理后的CPU的状态。 + +**程序计数器:**存储下一条指令所在内存的地址。 + +**基址寄存器:**存储数据内存的起始地址。 + +**变址寄存器:**存储基址寄存器的相对地址。 + +**通用寄存器:**存储任意数据。 + +**指令寄存器:**存储指令。CPU内部使用,程序员无法通过程序对该寄存器进行读写操作。 + +**栈寄存器:**存储栈区域的起始地址。 + +其中,程序计数器,累加寄存器,标志寄存器,指令寄存器和栈寄存器都只有一个,其他的寄存器一般有多个。 + + + +**计算机执行的原理是: 取指执行 ** + +处理器执行的程序是由一组保存在存储器中的指令组成的。最简单的指令处理包括两步: 处理器从存储器中一次读(取)一条指令,然后执行每条指令。程序执行是由不断重复的取指令和执行指令的过程组成的。指令执行可能涉及很多操作,具体取决于指令本身。 + +在典型的处理器中,程序计数器(Program Counter, PC)保存下一次要取的指令地址。除非出现其它情况,否则处理器在每次取值令后总是递增PC,以便能按顺序取下一条指令(即位于下一个存储器地址的指令)。取到的指令放在处理器的一个寄存器中,这个寄存器称为指令寄存器(Instruction Register,IR)。指令中包含确定处理器将要执行的操作的位,处理器解释指令并执行对应的操作。大体上,这些动作可分为4类: + +- 处理器-存储器:数据可以从处理器传送到存储器,或从存储器传送到处理器。 +- 处理器-I/O:通过处理器和I/O模块间的数据传送,数据可以输出到外部设备,或从外部设备向处理器输入数据。 +- 数据处理:处理器可以执行很多与数据相关的算术操作或逻辑操作。 +- 控制: 某些指令可以改变执行顺序。例如,处理器从地址为149的存储单元中取出一条指令,该指令指向下一条指令应该从地址为182的存储单元中取,这样处理器就会把程序计数器置为182.因此在下一个取指阶段,将从地址182的存储单元而非150的存储单元中取指令。 + + + +## CPU 指令执行过程 + +那么 CPU 是如何执行一条条的指令的呢? + +几乎所有的冯·诺伊曼型计算机的CPU,其工作都可以分为5个阶段:**取指令、指令译码、执行指令、访存取数、结果写回**。 + +- `取指令`阶段是将内存中的指令读取到 CPU 中寄存器的过程,程序寄存器用于存储下一条指令所在的地址 +- `指令译码`阶段,在取指令完成后,立马进入指令译码阶段,在指令译码阶段,指令译码器按照预定的指令格式,对取回的指令进行拆分和解释,识别区分出不同的指令类别以及各种获取操作数的方法。 +- `执行指令`阶段,译码完成后,就需要执行这一条指令了,此阶段的任务是完成指令所规定的各种操作,具体实现指令的功能。 +- `访问取数`阶段,根据指令的需要,有可能需要从内存中提取数据,此阶段的任务是:根据指令地址码,得到操作数在主存中的地址,并从主存中读取该操作数用于运算。 +- `结果写回`阶段,作为最后一个阶段,结果写回(Write Back,WB)阶段把执行指令阶段的运行结果数据“写回”到某种存储形式:结果数据经常被写到CPU的内部寄存器中,以便被后续的指令快速地存取; + + + +## 存储器 + +在任何一种计算机中,第二种主要部件都是存储器。在理想情况下,存储器应该极为迅速(快于执行一条指令,这样CPU就不会受到存储器的限制),充分大并且非常便宜。但是目前的技术无法同时满足这三个目标。 + +- 存取时间越快,每“位”的价格越高 +- 容量越大,每“位”的价格越低 +- 容量越大,存取速度越慢 + + + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/computer_storage_type.png?raw=true) + +于是出现了多级存储器组织结构: + +- 寄存器: 最快、最小和最贵的存储器类型由位于处理器内部的寄存器组成。它们用预CPU相同的材料制成,所以和CPU一样快。显然,访问它们是没有时延的。其典型的存储容量是,在32位CPU中为32*32位,而在64位CPU中为64*64位。在这两种情况下,其存储容量都小于1KB。典型情况下,一个处理器包含多个寄存器,某些处理器包含上百个寄存器。 + +- 高速缓存,它多数由硬件控制。 + +- 主存:这是存储器系统的主力。主存通常称为随机访问存储器(Random Access Memory,RAM),内存是计算机中主要的内部内部存储器系统。内存中的每个单元位置都有唯一的地址对应,而且大多数机器指令会访问一个或多个内存地址。内存通常是告诉的、容量较小的告诉缓存的扩展。 + +- 磁盘:磁盘同RAM相比,成本降低了,但是随机访问数据时间也慢了。其低速的原因是因为磁盘是一种机械装置。一个磁盘中有一个或多个金属盘片,他们以一定的速度旋转,从边缘开始有一个机械臂悬横在盘面上,这类似于老式播放塑料唱片的唱片机。 + + + + ![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/changpianji.jpg?raw=true) + + 有时,还有一些实际上不是磁盘的磁盘,比如固态硬盘(Solid State Disk,SSD)。固态硬盘并没有可以移动的部分,外形也不像唱片那样,并且数据是存储在存储器(闪存)中的。与磁盘唯一的相似之处就是它也存储了大量即使在电源关闭时也不会丢失的数据。 + +这里要说一下闪存(flash memory),在便捷式电子设备中,闪存通常作为存储媒介。闪存是数吗相机中的胶卷。是便捷式音乐播放器的磁盘。这仅仅是闪存用途的两项。闪存在速度上介于RAM和磁盘之间。另外,与磁盘存储器不同的是,如果闪存擦除次数过多,就被磨损了。 + + + + + +## 内存 + + + +内存包括主存(内存条,基于DRAM(动态RAM))与高速缓存(Cache,基于SRAM(静态RAM,静态RAM速度很快但是成本很高,所以用于在CPU内部充当缓冲))两部分。可能是由于Cache相较内存条容量很小,毕竟内存容量只计内存条大小,加上重要性也不及内存条,一般人或许不知道Cache,所以就忽略了高速缓存Cache,直接将主存--内存条等同了内存吧。计算器内存条采用的是DRAM(动态随机存储器),即计算机的主存。通常所说的内存容量即指内存条DRAM的大小。 + +高速缓冲存储器Cache主要是为了解决CPU和主存速度不匹配而设计的。Cache一般由SRAM(静态随机存储器)芯片实现,它的存取速度接近CPU,快于DRAM,存储容量小于DRAM。它比主存的优先级高,CPU存取信息时优先访问Cache,找不到的话再去主存DRAM中找,同时把信息周围的数据块从主存复制到Cache中。 现代计算机系统基本都采用Cache-主存-辅存(即外存储器)三级存储系统。其中CPU可直接访问Cache和主存,辅存则通过主存与CPU交换信息。 + + + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/memory_type.png?raw=true) + + + +### 主存的分类 + +- RAM(Random-access memory) + + 随机存取存储器,一般使用动态半导体存储器件(DRAM),对于CPU来说,RAM是主要存放数据和程序的地方,所以也叫做“主存”,因为CPU工作的速度比RAM的读写速度快,所以CPU读写RAM时需要花费时间等待,这样就使CPU的工作速度下降。人们为了提高CPU读写程序和数据的速度,在RAM和CPU之间增加了高速缓存(Cache)部件。Cache的内容是随机存储器(RAM)中部分存储单元内容的副本 + +- ROM(Read-Only Memory) + + 只读存储器,出厂时其内容由厂家用掩膜技术写好,只可读出,但无法改写。信息已固化在存储器中,一般用于存放系统程序BIOS和用于微程序,断电也没有关系,放ROM的数据一辈子都不会变 + + + +### 缓存 + +位于CPU与内存之间的临时存储器,它的容量比内存小的多但是交换速率却比内存要快得多。缓存的出现主要是为了解决CPU运算速率与内存读写速率不匹配的矛盾,缓存往往使用的是RAM,L1 Cache(一级缓存)是CPU第一层高速缓存,一般L1缓存的容量通常在32—256KB,L1分为数据Cache,指令Cache,L2 Cache(二级缓存)是CPU的第二层高速缓存,分内部和外部两种芯片,内部的芯片二级缓存运行速率与主频相同,而外部的二级缓存则只有主频的一半,缓存只是内存中少部分数据的复制品。二级缓存是比一级缓存速率更慢,容量更大的内存,主要就是做一级缓存和内存之间数据临时交换的地方用。为了适应速率更快的处理器。 + + + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/cpu_cache_memory.png?raw=true) + + + +缓存基本上都是采用SRAM存储器,SRAM是英文Static RAM的缩写,它是一种具有静态存取功能的存储器,不需要刷新电路即能保存它内部存储的数据。不像DRAM内存那样需要刷新电路,每隔一段时间,固定要对DRAM刷新充电一次,否则内部的数据即会消失,因此SRAM具有较高的性能,但是SRAM也有它的缺点,即它的集成度较低,相同容量的DRAM内存可以设计为较小的体积,但是SRAM却需要很大的体积,这也是不能将缓存容量做得太大的重要原因。它的特点归纳如下:优点是节能、速率快、不必配合内存刷新电路、可提高整体的工作效率,缺点是集成度低、相同的容量体积较大、而且价格较高,只能少量用于关键性系统以提高效率。 + + + +缓存的工作原理是当CPU要读取一个数据时,首先从CPU缓存中查找,找到就立即读取并送给CPU处理;没有找到,就从速率相对较慢的内存中读取并送给CPU处理,同时把这个数据所在的数据块调入缓存中,可以使得以后对整块数据的读取都从缓存中进行,不必再调用内存。正是这样的读取机制使CPU读取缓存的命中率非常高,一般把静态RAM缓存叫一级缓存,而把后来增加的动态RAM叫二级缓存。 + + + + + + + +## 系统软件和应用软件 + + + +- 系统软件是指控制和协调计算机以及外部设备,支持应用软件开发和运行的系统,是无需用户干预的各种程序的集合。主要功能是调度、监控和维护计算机系统。例如: 操作系统和一系列的基本工具(编译器、数据库管理、存储器格式化、用户身份验证、网络连接)。 +- 应用软件是和系统软件相对的,是用户可以使用的各种程序设计语言,以及用各种程序设计语言编译的应用程序的集合,分为应用软件包和用户程序。例如:互联网软件、多媒体软件、协作软件等。 + + + + + + + + + + + + + + + + + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/OperatingSystem/2.\350\277\233\347\250\213.md" "b/OperatingSystem/2.\350\277\233\347\250\213.md" new file mode 100644 index 00000000..cad290fc --- /dev/null +++ "b/OperatingSystem/2.\350\277\233\347\250\213.md" @@ -0,0 +1,303 @@ +# 1.进程 + + + +狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。 + +广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是[操作系统](https://baike.baidu.com/item/操作系统/192)动态执行的[基本单元](https://baike.baidu.com/item/基本单元),在传统的[操作系统](https://baike.baidu.com/item/操作系统)中,进程既是基本的[分配单元](https://baike.baidu.com/item/分配单元),也是基本的执行单元。 + +进程的概念主要有两点:第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括[文本](https://baike.baidu.com/item/文本)区域(text region)、数据区域(data region)和[堆栈](https://baike.baidu.com/item/堆栈)(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有[处理](https://baike.baidu.com/item/处理)器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为[进程](https://baike.baidu.com/item/进程)。 + + + +进程是60年代初首先由[麻省理工学院](https://baike.baidu.com/item/麻省理工学院)的[MULTICS系统](https://baike.baidu.com/item/MULTICS系统)和IBM公司的[CTSS](https://baike.baidu.com/item/CTSS)/360系统引入的。 [2] + +进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。它可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体。它不只是程序的[代码](https://baike.baidu.com/item/代码),还包括当前的活动,通过[程序计数器](https://baike.baidu.com/item/程序计数器)的值和处理[寄存器](https://baike.baidu.com/item/寄存器)的内容来表示。 + +进程由三部分组成: + +- 一段可执行的程序 +- 程序所需要的相关数据(变量、工作空间、缓冲区等) +- 程序的执行上下文 + +最后一部分是根本。执行上下文(execution context)又称为进程状态(process state),是操作系统用来管理和控制进程所需的内部数据。这种内部信息和进程是分开的,因为操作系统信息不允许被进程直接访问。上下文包括操作系统管理进程及处理器正确执行进程所需的所有信息,包括各种处理器寄存器的内容,如程序计数器和数据寄存器。它还包括操作系统使用的信息,如进程优先级及进程是否在等待I/O事件的完成。 + + + +在任何多道程序设计系统中,CPU由一个进程快速切换至另一个进程,使每个进程各运行几十或几百毫秒。严格来说,在某一个瞬间,CPU只能运行一个进程。但在1秒钟内,它可能运行多个进程,这样就产生并行的错觉。CPU在各进程之间来回切换,这种快速的切换称为多道程序设计。 + +## 多道程序设计模型 + +采用多道程序设计可以提高CPU利用率。严格地说,如果进程用于计算的平均时间是进程在内存中停留时间的20%,且内存中同时有5个进程,则CPU将一直满负载运行。然而,这个模型在现实中过于乐观,因为它假设这5个进程不会同时等待I/O。 + +更好的模型是从概率的角度来看CPU的利用率。假设一个进程等待I/O操作的时间与其停留在内存的时间比为p。当内存中同时有n个进程时,则所有n个进程都在等待I/O(此时CPU空转)的概率为p的n次方。 + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/duodao_module.png?raw=true) + +从上图中可以看到,如果进程花费80%的时间等待I/O,为使CPU的浪费低于10%,至少要有10个进程同时在内存中。 + + + +## 进程的特性 + +1. 动态性 + + 动态性是进程的最基本特征,它是程序执行过程,它是有一定的生命期。它由创建而产生、由调度而执行,因得不到资源而暂停,并由撤消而死亡。而程序是静态的,它是存放在介质上一组有序指令的集合,无运动的含义。 + +2. 并发性 + + 并发性是进程的重要特征,同时也是OS的重要特征。并发性指多个进程实体同存于内存中,能在一段时间内同时运行。而程序是不能并发执行。 + +3. 独立性 + + 进程是一个能独立运行的基本单位,即是一个独立获得资源和独立调度的单位,而程序不作为独立单位参加运行。 + +4. 异步性 + + 进程按各自独立的不可预知的速度向前推进,即进程按异步方式进行,正是这一特征,将导致程序执行的不可再现性,因此OS必须采用某种措施来限制各进程推进序列以保证各程序间正常协调运行。 + + + +进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对 PCB 的操作。 + +## 进程的状态 + + + +- 就绪态:进程做好了准备,只要有机会就开始执行 +- 运行态:进程正在执行 +- 堵塞/等待态:进程在某些事件发生前补鞥呢执行,如I/O操作完成 +- 新建态:刚刚创建的进程,操作系统还未把他加入可执行进程组,它通常是进程控制块已经创建但还未加载到内存中的新进程 +- 退出态:操作系统从可执行进程组中释放出的进程,要么它自身已停止,要么它因某种原因被取消 + +### 进程由哪几部分组成? + +进程是程序的一次运行过程,它是由程序段、数据段和进程控制块 PCB 组成的一个实体,其中: + +- 程序段:对应程序的操作代码部分,用于描述进程所需要完成的功能。 +- 数据段:对应程序执行时所需要的数据部分,包括数据,堆栈和工作区。 +- 进程控制块:记录了进程运行时所需要的全部信息,它是进程存在的唯一标识,与进程一一对应。 + +##### 进程控制块 + +进程执行的任意时刻,都可由如下元素来表征: + +- 标识符:与进程相关的唯一标识符,用来区分其他进程 +- 状态:若进程正在执行,则进程处于运行态 +- 优先级:相对于其他进程的优先顺序 +- 程序计数器:程序中即将执行的下一条指令的地址 +- 内存指针:包括程序代码和进程相关数据的指针,以及与其他进程共享内存块的指针 +- 上下文数据:进程执行时处理器的寄存器中的数据 +- I/O状态信息:包括显式I/O请求、分配给进程的I/O设备和被进程使用的文件列表等 +- 记账信息:包括处理器时间总和、使用的时钟数总和、时间限制、及账号等 + +上述列表信息存放在一个被称为进程控制块(process control block)的数据结构中,控制块由操作系统创建和管理。进程控制块(PCB)是进程实体的重要组成部分,它记录了操作系统所需要的、用于描述进程情况及控制进程所需要的全部信息。原来不能独立运行的程序或数据,通过 PCB 就可以成为一个可以独立运行的基本单位。系统通过 PCB 感知进程的存在,并对其进行有效管理和控制。系统创建一个新进程时,为它建立一个 PCB;当进程结束时,系统又收回其 PCB,该进程也随之消亡。简单的总结就是进程控制块主要包括下述四个方面的信息: + +1. 进程标识符信息:用于标识、区分一个进程,通常有外部标识符和内部标识符两类。外部标识符通常是由字母、数字所组成的一个字符串,用户或其他进程访问该进程时使用。内部标识符是操作系统为每个进程赋予的唯一一个整数,是作为内部识别而设置的。 + +2. 进程调度信息:用于描述与进程调度有关的状态信息,包括进程状态、进程优先权、调度信息和等待事件等。进程状态指明进程当前的状态,作为进程调度和对换时的依据;进程优先权说明进程使用 CPU 的优先级别,其中优先权高的进程将优先获得 CPU;调度信息描述与进程调度算法相关的信息,如进程等待时间、已运行的时间等;等待事件是指进程由运行态转变为阻塞态时所等待发生的事件。 + +3. CPU 状态信息:用于保留进程运行时 CPU 的各种信息,使得进程暂停运行后,下次重新运行时能从上次停止的地方继续运行。CPU 状态信息通常包含通用寄存器、控制和状态寄存器、用户栈指针等。CPU 状态字记载了程序执行的状态信息,如条件码、外中断屏蔽标识、执行状态(核心态或用户态)标识等。 + +4. 进程控制信息(process control information):包括进程资源、控制机制等一些进程运行时所需要的信息,如: + - 程序和数据地址:该进程的程序和数据所在的内存和外存地址,以便该进程在次运行时,能够找到程序和数据。 + - 进程同步和通信机制:实现进程同步和通信时所采用的机制,如消息队列指针、信号量等。 + - 资源清单:除 CPU 外,进程所需的全部资源和已经分配到的资源。 + - 链接指针:用于指向该进程所在队列的下一个进程的 PCB 首地址。 + + + +## 进程创建 + +操作系统决定创建一个新进程时,会按如下步骤操作: + +1. 为新进程分配一个唯一的进程标识符。此时,主进程表中会添加一个新表项,每个进程一个表项。 +2. 为进程分配空间。这包括进程映像中的所有元素。因此,操作系统必须知道私有用户地址空间(程序和数据)和用户栈需要多少空间。 +3. 初始化进程控制块。进程表示部分包括进程id和其他相关的id,如父进程id等。处理器状态信息部分的多数项目通常初始化为0,但程序计数器(置为程序入口点)和系统栈指针(定义进程栈边界)除外。进程控制信息部分根据标准的默认值和该进程请求的特性来初始化。例如,进程的状态通常初始化为就绪或就绪/挂起。 +4. 设置正确的链接。例如,若操作系统将每个调度队列都维护为一个链表,则新进程必须放在继续或就绪/挂起链表中。 +5. 创建或扩充其他数据结构。例如,操作系统可因编制账单和/或评估性能,为每个进程维护一个记账文件。 + + + +## 进程切换 + + + +表面上看,进程切换很简单。在某个时刻,操作系统中断一个正在运行的进程,将另一个进程置于运行模式,并把控制权交给后者。然而,这会引发若干个问题。首先,什么事件触发了进程的切换? 其次,必须认识到模式切换和进程切换键的区别。 + + + +进程切换可在操作系统从当前正运行进程中获得控制权的任何时刻发生。首先考虑系统终端。实际上,大多数操作系统都会区分两种系统终端:一种称为终端,另一种称为陷阱。前者与当前正运行进程无关的某种外部事件相关,如完成一次I/O操作。后者与当前正运行进程产生的错误或异常条件相关,如非法的文件访问。对于普通终端,控制权首先转给终端处理器,终端处理器完成一些基本的辅助工作后,再将控制权给与已发生的特定中断相关的操作系统进程。示例如下: + +- 时钟中断:操作系统确定当前正运行进程的执行时间是否已超过最大允许时间段(时间片,即进程中断前可以执行的最大时间段)。若超过,进程就切换到就绪态,并调入另一个进程。 +- I/O中断:操作系统确定是否已发生I/O活动。若I/O活动是一个或多个进程正在等待的事件,则操作系统就把所处于堵塞态的进程转换为就绪态( 堵塞/挂起态进程转换为就绪/挂起态)。操作系统必须决定是继续执行当前处于运行态的进程,还是让具有高优先级的就绪态进程抢占这个进程。 +- 内存失效:处理器遇到一个引用不在内存中的字的虚存地址时,操作系统就必须从外村中把包含这一引用的内存块(页或段)调入内存。发出调入内存块的I/O请求后,内存失效进程将进入堵塞态。操作系统然后切换进程,恢复另一个进程的执行。期望的块调入内存后,该进程置为就绪态。 + +对于陷阱(trap),操作系统则确定错误或异常条件是否致命。致命时,当前正运行进程置为退出态,并切换进程。不致命时,操作系统的动作将取决于错误的性质和操作系统的设计,操作系统可能会尝试恢复程序,或简单的通知用户。操作系统可能会切换进程或继续当前运行的进程。 + + + +## 并发 + +在单处理器多道程序设计系统中,进程会被交替的执行,因而表现出一种并发执行的外部特征。即使不能实现真正的并行处理,并且在进程间来回切换也需要一定的开销,但是交替执行在处理效率和程序结构上还是会带来很多好处。在多处理系统中,不仅可以交替的执行进程,而且可以重叠执行进程。 + +进程的相对执行速度不可预测,它取决于其他进程的活动、操作系统处理中断的方式以及操作系统的调度策略,这样就会有以下问题: + +1. 全局资源的共享充满了危险。例如,如果两个进程都使用同一个全局变量,并且都对该变量执行读写操作,那么不同的读写执行顺序是非常关键的。 +2. 操作系统很难对资源进行最优化分配。例如,进程A可能请求使用一个特定的I/O通道,并获取控制权,但它在使用这个通道前已被堵塞,而操作系统仍然锁定这个通道以防止其他进程使用,这是最难以令人满意的。事实上,这种情况有可能导致死锁。 +3. 定位程序设计错误非常困难。这是因为结果通常是不确定的和不可再现的。 + +所以由于并发带来的这些问题,操作系统必须关注的问题如下: + +1. 操作系统必须能够跟踪不同的进程,这可以使用进程控制块来实现。 +2. 操作系统必须为每个活动进程分配和释放各种资源。 +3. 操作系统必须保护每个进程的数据和物理资源,避免其他进程的无意干扰。 +4. 一个进程的功能和输出结果必须与执行速度无关。 + + + +### 互斥的要求 + +1. 必须强制实施互斥。在于相同资源或共享对象的临界区有关的所有进程中,一次只允许一个进程进入临界区。 +2. 一个在非临界区停止的进程不能干啥其他进程。 +3. 绝不允许出现需要访问临界区的进程被无限延迟的情况,即不会死锁或饥饿。 +4. 没有进程在临界区中时,任何需要进入临界区的进程必须能够立即进入。 +5. 对相关进程的执行速度和处理器的数量没有任何要求和限制。 +6. 一个进程驻留在临界区中的时间必须是有限的。 + + + +## 进程间通信(Inter Process Communication,IPC) + + + +进程间的信息交换,具体内容分为:控制信息交换和数据交换,控制信息的交换为低级通信,数据的交换为高级通信。 + + + +高级通信方式 + +- 共享存储系统 + +多台服务器访问同一个存储设备的同一分区 + +- 消息传递系统 + +进程与其它的进程进行通信而不必借助共享数据,通过互相发送和接收消息,建立一条通信链路。 + +- 管道通信 + +发送进程以字符流形式将大量数据送入管道,接收进程可从管道接收数据,二者利用管道进行通信,管道是单向的、先进先出的,它把一个进程的输出和另一个进程的输入连接在一起。一个进程(写进程)在管道的尾部写入数据,另一个进程(读进程)从管道的头部读出数据。每次只有一个进程能够真正地进入管道,其他的只能等待。 + +管道分为无名管道和命名管道,前者用于父子进程通信,后者用于任意进程通信。 + + + +**操作系统最底层的就是调度程序**,在它上面有许多进程。所有关于中断处理、启动进程和停止进程的具体细节都隐藏在调度程序中。事实上,调度程序只是一段非常小的程序。 + + + +### 进程的实现 + +操作系统为了执行进程间的切换,会维护着一张表格,这张表就是 `进程表(process table)`。每个进程占用一个进程表项。该表项包含了进程状态的重要信息,包括程序计数器、堆栈指针、内存分配状况、所打开文件的状态、账号和调度信息,以及其他在进程由运行态转换到就绪态或阻塞态时所必须保存的信息,从而保证该进程随后能再次启动,就像从未被中断过一样。 + + + +**操作系统最底层的就是调度程序**,在它上面有许多进程。所有关于中断处理、启动进程和停止进程的具体细节都隐藏在调度程序中。事实上,调度程序只是一段非常小的程序。 + + + +操作系统为了执行进程间的切换,会维护着一张表格,这张表就是 `进程表(process table)`。每个进程占用一个进程表项。该表项包含了进程状态的重要信息,包括程序计数器、堆栈指针、内存分配状况、所打开文件的状态、账号和调度信息,以及其他在进程由运行态转换到就绪态或阻塞态时所必须保存的信息,从而保证该进程随后能再次启动,就像从未被中断过一样。 + + + +## 进程调度 + +进程调度就是处理器调度(上下文切换) + +### 调度级别 + +- 高级调度 + +作业调度,把后备作业调入内存运行 + +- 中级调度 + +在虚拟存储器中引入,在内,外存交换区进行进程对换 + +- 低级调度 + +进程调度,把就绪队列里的某个进程获得CPU执行权 + +### 调度方式 + +- 可剥夺 + + 当一个进程运行时,基于某种原则,剥夺已经分配给它的处理器,将之分配给其他进程,原则有:优先权原则,短进程优先原则,时间片原则。 + +- 不可剥夺 + +一单处理器分配给某进程,遍让它一直运行下去,直到进程完成或者发生某种时间而阻塞,才分配给其他进程。 + +### 调度算法 + +- 先进先出 + +按照进入就绪队列的进程顺序,不加其他条件干涉 + +- 短进程优先 + +优先选出就绪队列中CPU执行时间最短的进程,例如:就绪队列有4个进程P1,P2,P3,P4,执行时间为:16,12,4,3 按照短进程优先,则周转时间(从进程提交到进程完成的时间间隔)分别为:35,19,7,3 +平均周转时间:16,平均周转时间越小,调度性能越好 + +- 轮转法 + - 简单轮转: 就绪进程按FIFO排队,按照一定时间间隔让处理机分配给队列中的进程,就绪队列中**所有队列均可获得一个时间片的处理器运行** + - 多级队列: 让系统中所有进程分成若干类,每类一级 + +## 死锁 + +两个以上的进程相互请求对方已经占有的资源,导致无限期的等待。 + +### 产生条件 + +- 互斥条件 +- 请求保持 +- 不可剥夺 +- 环路条件 + +### 解决方法 + +- 鸵鸟策略:不理睬 +- 预防策略:破坏产生条件中任意一个 +- 避免策略: 精心分配资源,动态避免死锁 +- 检测与解除死锁:系统自动检测,并且解除 + + + + + + + +## 进程和线程的区别 + +线程比进程更轻量级,所以它们比进程更容易(更快)创建,也更容易撤销。在许多系统中,创建一个线程比创建一个进程要快10~100倍。 + + + +进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。 + +1. 简而言之,一个程序至少有一个进程,一个进程至少有一个线程。 +2. 线程的划分尺度小于进程,使得多线程程序的并发性高。 +3. 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。 +4. 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。 +5. 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。 + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" "b/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" new file mode 100644 index 00000000..a4b40e39 --- /dev/null +++ "b/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" @@ -0,0 +1,277 @@ +# 3. 内存管理 + +常用概念: + +- 页框:内存中固定长度的快 +- 页:固定长度的数据库,存储在二级存储器中(如磁盘)。数据页可以临时赋值到内存的页框中。 +- 段:变长数据块,存储在二级存储器中。整个段可以临时复制到内存的一个可用区域中(分段),或可以将一个段分为许多页,然后将每页单独复制到内存中(分段与分页相结合) + + + +内存管理的主要操作是处理器把程序装入内存中执行。内存管理的功能有: +1、内存空间的分配与回收:由操作系统完成主存储器空间的分配和管理,使程序员摆脱存储分配的麻烦,提高编程效率。 +2、地址转换:在多道程序环境下,程序中的逻辑地址与内存中的物理地址不可能一致,因此存储管理必须提供地址变换功能,把逻辑地址转换成相应的物理地址。 +3、内存空间的扩充:利用虚拟存储技术或自动覆盖技术,从逻辑上扩充内存。 +4、存储保护:保证各道作业在各自的存储空间内运行,互不干扰。 + + + +进程对应的内存空间中所包含的5种不同的数据区: + +- 代码段(code segment):又称文本段,用来存放指令,运行代码的一块内存空间。此空间大小在代码运行前就已经确定。内存空间一般属于只读,某些架构的代码也允许可写。 + +- 数据段(data segment): 存储初始化的全局变量和初始化的static变量。数据段中的数据的生存期是随程序持续性(随进程持续性):进程创建就存在,进程死亡就消失。 + +- BSS段(bss segment):存储未初始化的全局变量和未初始化的static变量。bss段中数据的生存期随进程持续性。bss段中的数据一般默认为0. +- rodata段:只读数据 比如 printf 语句中的格式字符串和开关语句的跳转表。也就是常量区。例如,全局作用域中的 const int ival = 10,ival 存放在 .rodata 段;再如,函数局部作用域中的 printf("Hello world %d\n", c); 语句中的格式字符串 "Hello world %d\n",也存放在 .rodata 段。 + +- 堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc、realloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减) + +- 栈(stack):栈是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。栈的生存期随代码块持续性,代码块运行就给你分配空间,代码块结束就自动回收空间。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。 + +上述几种内存区域中数据段、BSS和堆通常是被连续存储的——内存位置上是连续的,而代码段和栈往往会被独立存放。有趣的是,堆和栈两个区域关系很“暧昧”,他们一个向下“长”(i386体系结构中栈向下、堆向上),一个向上“长”,相对而生。但你不必担心他们会碰头,因为他们之间间隔很大(到底大到多少,你可以从下面的例子程序计算一下),绝少有机会能碰到一起。 + + + +## 内存的演变 + +在早些的操作系统中,并没有引入内存抽象的概念。程序直接访问和操作的都是物理内存,内存的管理也非常简单,除去操作系统所用的内存之外,全部给用户程序使用,想怎么折腾都行,只要别超出最大的容量。这种内存操作方式使得操作系统中存在多进程变得完全不可能,比如MS-DOS,你必须执行完一条指令后才能接着执行下一条。如果是多进程的话,由于直接操作物理内存地址,当一个进程给内存地址1000赋值后,另一个进程也同样给内存地址赋值,那么第二个进程对内存的赋值会覆盖第一个进程所赋的值,这回造成两条进程同时崩溃。随着计算机技术发展,要求操作系统支持多进程的需求,所谓多进程,并不需要同时运行这些进程,只要它们都处于 ready 状态,操作系统快速地在它们之间切换,就能达到同时运行的假象。每个进程都需要内存,Context Switch 时,之前内存里的内容怎么办?简单粗暴的方式就是先 dump 到磁盘上,然后再从磁盘上 restore 之前 dump 的内容(如果有的话),但效果并不好,太慢了!那怎么才能不慢呢?把进程对应的内存依旧留在物理内存中,需要的时候就切换到特定的区域。这就涉及到了内存的保护机制,毕竟进程之间可以随意读取、写入内容就乱套了,非常不安全。因此操作系统需要对物理内存做一层抽象,也就是「地址空间」(Address Space),一个进程的地址空间包含了该进程所有相关内存,比如 code / stack / heap。一个 16 KB 的地址空间可能长这样: + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/memory_1.jpg?raw=true) + +Stack 和 Heap 中间有一块 free space,即使没有用,也被占着,那如何才能解放这块区域呢,进入虚拟内存。 + + + +Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自互不干涉的进程地址空间。该空间是块大小为4G的线性虚拟空间,用户所看到和接触到的都是该虚拟地址,无法看到实际的物理内存地址。利用这种虚拟地址不但能起到保护操作系统的效果(用户不能直接访问物理内存),而且更重要的是,用户程序可使用比实际物理内存更大的地址空间(具体的原因请看硬件基础部分)。 + +在讨论进程空间细节前,这里先要澄清下面几个问题: + +l 第一、4G的进程地址空间被人为的分为两个部分——用户空间与内核空间。用户空间从0到3G(0xC0000000),内核空间占据3G到4G。用户进程通常情况下只能访问用户空间的虚拟地址,不能访问内核空间虚拟地址。只有用户进程进行系统调用(代表用户进程在内核态执行)等时刻可以访问到内核空间。 + +l 第二、用户空间对应进程,所以每当进程切换,用户空间就会跟着变化;而内核空间是由内核负责映射,它并不会跟着进程改变,是固定的。内核空间地址有自己对应的页表(init_mm.pgd),用户进程各自有不同的页表。 + +l 第三、每个进程的用户空间都是完全独立、互不相干的。不信的话,你可以把上面的程序同时运行10次(当然为了同时运行,让它们在返回前一同睡眠100秒吧),你会看到10个进程占用的线性地址一模一样。 + + + +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9pbWFnZXMyMDE3LmNuYmxvZ3MuY29tL2Jsb2cvMTMxMTQxMi8yMDE4MDIvMTMxMTQxMi0yMDE4MDIxNTEyMjY1NDM1OS0xNDcxMTE4NzM4LnBuZw?x-oss-process=image/format,png) + + + +### 固定分区 + + + +大多数内存管理方案都假定操作系统占据内存中的某些固定部分,而内存中的其余部分则供多个用户进程使用。管理用户内存空间的最简单的方案就是对它分区,以形成若干边界固定的区域。 + +固定分区分为两种: + +- 使用大小相等的分区:此时小于等于分区大小的任何进程都可以装入任何可用的分区中。若所有分区都已满且没有进程处于就绪态或运行态,则操作系统可以换出一个进程的所有分区,并装入另一个进程,使得处理器有事可做。但是大小相等的分区有两个难点: + + - 程序可能太大而不能放到一个分区中。此时程序员必须使用覆盖技术设计程序,使得任何时候该程序只有一部分需要放到内存中。当需要的模块不在时,用户程序必须把这个模块装入程序的分区,覆盖该分区中的任何程序和数据。 + + - 内存的利用率非常低。任何程序,急事很小,都需要占据一个完整的分区。由于装入的数据块小于分区大小,因而导致分区内部存在空间浪费,这种现象称为内部碎片(internal fragmentation)。 + +- 使用大小不等的分区:大小不等的分区可以缓解上面的两个问题,但是不能完全解决。 + +虽然大小不等的分区带来了一定的灵活性,但是分区的数量在系统生成阶段已经确定,因而限制了系统中活动(未挂起)进程的数量。而且由于分区大小是在系统生成阶段事先设置的,因而小作业不能有效的利用分区空间。 + + + +### 动态分区 + +为了克服固定分区的确定,提出了动态分区。对于动态分区,分区长度和数量是可变的。程序装入内存时,系统会给它分配一块与其所需容量完全相等的内存空间。但是对于一个64MB的内存,如果装入前三个进程已经占用了很大一部分,剩下的部分对于第四个进程来说又太小,这样在内存的末尾就剩下一个“空洞”。而等第二个进程结束后腾出的足够的空间来装入第四个进程,但是由于第四个进程比第二个进程小,所有这里又形成了另一个小"空洞"。这样最终在内存中就会形成许多小空洞。随着时间的推移,内存中形成了越来越多的碎片,内存的利用率随之下降。这种现象称为外部碎片(external fragmentation),指在所有分区外的存储空间变成了越来越多的碎片,这与前面所讲的内部碎片正好对应。 + +客服外部碎片的一种技术就是压缩(compaction)。操作系统不时的移动进程,使得进程占用的空间连续,并使所有空闲空间连成一片。但是压缩的困难之处在于,它是一个非常费时的过程,且会浪费处理器时间。 + + + +### 伙伴系统 + +固定分区和动态分区方案都有缺陷。固定分区方案限制了活动进程数量,且如果可用分区的大小与进程大小很不匹配,那么内存空间的利用率会非常低。动态分区的维护特别复杂,并且会引入进行压缩的额外开销。更有吸引力的一种折中方案是伙伴系统。 + + + +### 重定位 + + + +在大小相等的分区中一个进程在其声明周期中可能占据不同的分区。首次创建一个进程映像时,它被装入内存中的摸个分区。以后该进程可能被换出,当它再次被换入时,可能被指定到与上一次不同的分区中。动态分区也存在同样的情况,压缩后内存中的进程也可能发生移动。因此,进程访问(指令和数据单元)的位置不是固定的。进程被换入或在内存中移动时,指令和数据单元的位置也发生变化。为了解决这个问题,需要区分几种地址类型: + +- 逻辑地址(logical address)是指与当前数据再内存中的物理分配地址无关的访问地址,在执行对内存的访问之前必须把它转换为物理地址。 +- 相对地址(relative address)是逻辑地址的一个特例,他是相对于某些已知点(通常是程序的开始处)的存储单元。 +- 物理地址(physical address)或绝对地址是数据在内存中的实际位置。 + +系统采用运行时动态加载的方式把使用相对地址的程序加载到内存。通常情况下,被加载进程中所有内存访问都相对于程序的开始点。因此,在执行包括这类访问的指令时,需要有把相对地址转换为物理内存地址的硬件机制。这类地址转换就需要一个特殊的处理器寄存器(基址寄存器),其内容是程序在内存中的起始地址。还有一个界限寄存器指明程序的终止位置。 + + + +### 分页 + +大小不等的固定分区和大小可变的分区技术在内存的使用上都是低效的,前者会产生内部碎片,后者会产生外部碎片。但是,如果内存被划分成大小固定、相等的块,切块相对比较小,每个进程也被分成同样大小的小块,那么进程中称为页的块可以分配到内存中称为页框的可用块。这样在使用分页技术时,每个进程在内存中浪费的空间,仅仅是进程最后一页的一小部分形成的内部碎片。没有任何外部碎片。 + +这样操作系统需要为每个进程维护一个页表(page table)。页表给出了该进程的每页所对应页框的位置。在程序中每个逻辑地址包括一个页号和该页中的偏移量。在分页中,逻辑地址到物理地址的转换仍然由处理器硬件完成,给出逻辑地址(页号,偏移量)后,处理器使用页表产生物理地址(页框号,偏移量)。 + + + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/page_memory_1.png?raw=true) + +采用分页技术的分区相当小,一个程序可以占据多个分区,并且这些分区不需要是连续的。 + +为了使分页方案更加方便,规定页和页框的大小必须是2的幂,以便容易的表示出相对地址。 + + + +### 分段 + +细分用户程序的另一种可选方案是分段。采用分段技术,可以把程序和与其相关的数据划分到几个段(fragment)中。尽管短有最大长度限制,但并不要求所有程序的所有段的长度都相等。和分页一样,采用分段技术时的逻辑地址也是由两部分组成:段号和偏移量。 + +由于使用大小不等的段,分段类似于动态分区。在未采用覆盖方案或使用虚存的情况下,为执行一个程序,需要把它的所有段都装入内存。与动态分区不同的是,在分段方案中,一个程序可以占据多个分区,并且这些分区不要求是连续的。分段消除了内部碎片,但是和动态分区一样,他会产生外部碎片。不过由于进程被分成多个小块,因此外部碎片也会很小。 + +采用大小不等的段的另一个结果是,逻辑地址和物理地址间不再是简单的对应关系。类似于分页,在简单的分段方案中,每个进程都有一个段表,系统也会维护一个内存中的空闲块列表。每个段表都必须给出相应段在内存中的起始地址,还必须指明段的长度,以确保不会使用无效地址。 + + + + + +分页和分段的两个特点: + +- 进程中的所有内存访问的都是逻辑地址,这些逻辑地址会在运行时动态地址转换为物理地址。这意味着一个进程可被换入或换出内存,因此进程可在执行过程中的不同时刻占据内存中的不同区域。 +- 一个进程可划分为许多块(页和段),在执行过程中,这些块不需要连续的位于内存中。动态运行时地址转换和页表或段表的使用使得这一点成为可能。 + +由于上面这两个特点的存在,那么在一个进程的执行过程中,该进程不需要所有页或段都在内存中。如果内存中保存有待取的下一条指令所在块(段或页)及待访问的下一个数据单元所在块,那么执行至少可以暂时继续下去。我们用术语“块”来表示页或段。处理器在需要访问一个不在内存中的逻辑地址时,会产生一个中断,这表明出现了内存访问故障。操作系统会把被中断的进程置于堵塞态。要继续执行这个进程,操作系统必须把包含引发访问故障的逻辑地址的进程块读入内存。为此操作系统产生一个磁盘I/O读请求。产生I/O请求后,在执行磁盘I/O期间,操作系统可以调度另一个进程运行。在需要的块读入内存后,产生一个I/O中断,控制权交回给操作系统,而操作系统则把由于缺少该块而被堵塞的进程置为就绪态。这样的话就会有两种提高系统利用率的方法: + +- 在内存中保留多个进程。由于对任何特定的进程都仅装入它的某些块,因此有足够的空间来放置更多的进程。这样,在任何时刻这些进程中至少有一个处于就绪态,于是处理器就得到了更有效的利用。 +- 进程可以比内存的全部空间还大。程序占用的内存空间的大小是程序设计的最大限制之一。没有这种方案时,程序员必须清楚的知道有多少内存空间可用。若编写的程序太大,程序员就必须设计出能把程序分成块的方法,这些块可按某种覆盖策略分别加载。通过基于分页或分段的虚拟内存,这项工作可由操作系统和硬件完成。对程序员而言,他所处理的是一个巨大的内存,大小与磁盘存储器有关。操作系统在需要时会自动的把进程块装入内存。 + +由于进程只能在内存中执行,因此这个存储器成为实存储器(real memory),简称实存。但程序员或用户感觉到的是一个更大的内存,且通常分配在磁盘上,这称为虚拟内存(virtual memory),简称虚存。虚存支持更有效的系统并发度,并能解除用户与内存之间没有必要的紧密约束。 + + + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/page_vs_fragment.png?raw=true) + + + + + +## 虚拟内存 + +面对越来越大的程序,常常产生程序>内存的问题,为解决这种问题,虚拟内存的概念得到普及. + +要有效的使用处理器和I/O设备,就需要在内存中保留尽可能多的进程。此外,还需要解除程序在开发时对程序使用内存大小的限制。解决这两个问题的途径就是虚拟内存技术。采用虚拟内存技术时,所有的地址访问都是逻辑访问,并在运行时转换为实地址。 + +虚拟内存机制使得期望运行大于物理内存的程序称为可能。其方法是将程序放在磁盘上,而将主存作为一种缓存,用来保存最频繁使用的部分程序。这种机制需要快速的映像内存地址,以便把程序生成的地址转换为有关字节在RAM中的物理地址。这种映像由CPU中的一个称为存储器管理单元(Memory Management Unit,MMU)的部件来完成。 + +**虚拟内存**的**基本思想**是:每个程序都拥有自己的地址空间,这个空间被分割成多个 块,每个块被成为一页或页面. + +**程序运行时,并不是所有页都在物理内存中**: + +- 当程序引用一部分在物理内存的地址空间时,由硬件直接执行必要的映射; +- 当程序引用一部分不在物理内存的地址空间时,有操作系统将缺失的页装入物理内存,并重新运行 + +虚拟内存基于分段和分页这两种基本技术或基于这两种技术中的一种。虚拟内存机制允许程序以逻辑方式访问存储器,而不考虑物理内存上可用的空间数量。虚存的构想是为了满足有多个用户作业同时驻留在内存中的要求,因此在一个进程被写出到副主存储器中且后续进程被读入时,连续的进程执行之间将不会脱节。进程大小不同时,若处理器在很多进程间切换,则很难把它们紧密的压入内存,因此人们引入了分页系统。在分页系统中,进程由许多固定大小的块组成。这些块成为页。程序通过虚地址(virtual address)访问,虚地址由页号和页中的偏移量组成。进程的每页都可置于内存中的任何地方,分页系统提供了程序中使用的虚地址和内存中的实地址(real address)或物理地址之间的动态映射。 + + + +### 分页访问过程: + +1. CPU中包含**MMU内存管理单元**,用于管理虚拟地址空间到物理内存地址的映射. +2. 假设物理内存地址大小为32k,每4k为一个页框.虚拟地址空间分页,每个页面大小等于一个页框 +3. 当程序想要访问一个虚拟地址x, +4. 指令将x送到MMU, +5. MMU根据x的虚拟地址,判断其对应的页面是否在物理内存中: +6. 若在,MMU将x转化为物理内存地址y +7. 若不在,则进行缺页中断,操作系统在物理内存中找到一个使用较少的页面回收掉,将需要访问的页面读到被回收的页面处,再将x转化为物理内存地址访问 + + + + + +### 虚拟内存的操作系统策略 + +#### 1. 读取策略 + +读取策略决定某页何时取入内存,常用的方法有如下两种: + +- 请求分页 + + 只有当访问到某页中的一个单元时才将改页取入内存。若内存管理的其他策略比较合适,将发生下述情况:当一个进程首次启动时,会在一段时间出现大量的缺页中断,取入越来越多的页后,局部性原理表明大多数将来访问的页都是最近去读的页。因此在一段时间后错误会逐渐减少,缺页中断的数量会降低到很低。 + +- 预先分页 + + 对于预先分页,读取的页并不是缺页中断请求的页。预先分页利用了大多数辅存设备(如磁盘)的特性,这些设备有寻道时间和合理的延迟。若一个进程的页连续存储在辅存中,则一次读取许多连续的页要比隔一段时间读取一页有效。当然,若大多数额外读取的页未引用到,则这个策略是低效的。进程首次启动时,可采用预先分页策略,此时程序员须以某种方式制定需要的页。 + +#### 2. 放置策略 + +防止策略决定一个进程块驻留在实存中的什么位置。在纯分页系统或段页式系统中,如何放置通常无关紧要,因为地址转换硬件和内存访问硬件能以相同的效率为任何页框组合执行相应的功能。 + +#### 3. 置换策略 + +以便处理在必须读取一个新页时,应该置换内存中的哪一页。当内存中的所有页框都被占据,且需要读取一个新页以处理一次缺页中断时,置换策略决定置换当前内存中的哪一页。所有策略的目标都是移出最近最不能访问的页。 + + + +#### 4. 驻留集管理 + +对于分页式虚拟内存,在准备执行时,不需要也不可能把一个进程的所有页都读入内存。因此,操作系统必须决定读取多少页,即决定给特定的进程分配多大的内存空间。 + + + +#### 5.清楚策略 + +与读取策略相反,清楚策略用于确定何时将已修改的一页写回辅存。通常有两种选择: + +- 请求式清楚 + + 只有当一页被选择用于置换时才能被写回辅存。 + +- 预约式清楚 + + 将这些已修改的多页在需要使用他们所占据的页框之前成批写回辅存。 + +#### 6.加载控制 + +加载控制会影响到驻留内存中的进程数量,这称为系统并发度。加载控制策略在有效的内存管理中非常重要。如果某一时刻驻留的进程太少,那么所有进程都处于堵塞态的概率就较大,因为会有许多时间话费在交换上。另一方面,如果驻留的进程太多,平均每个进程的驻留集大小将会不够用,此时会频繁发生缺页中断,从而导致系统抖动。 + + + + + +## Android内存管理 + +Android包含了标准Linux内核中内存管理设施的许多扩展,具体如下: + +- ASHMem:这个功能提供匿名共享内存,它将内存抽象为文件描述符。文件描述符可以传递给另一个进程以共享内存。 +- Pmen:这个功能分配虚拟内存,使的它在物理上是连续的,因此对于那些不支持虚拟内存的设备非常实用。 +- Low Memory Killer:大部分移动设备不具备置换能力(因为闪存的使用寿命因素)。主存耗尽时,使用大量内存的应用不是让步对内存的使用就是被终结。这个功能可让系统通知应用释放内存,如果应用不配合,则终结应用。 + + + + + + + + + + + + + + + + + + + + + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/OperatingSystem/4.\350\260\203\345\272\246.md" "b/OperatingSystem/4.\350\260\203\345\272\246.md" new file mode 100644 index 00000000..bb435432 --- /dev/null +++ "b/OperatingSystem/4.\350\260\203\345\272\246.md" @@ -0,0 +1,87 @@ +# 4.调度 + + + +在多道程序设计系统中,内存中有多个进程,每个进程要么正在处理器上运行,要么正在等待某些事件的发生,比如I/O完成。处理器调度的目的是:以满足系统目标(如响应时间、吞吐率、处理器效率)的方式,把进程分配到一个或多个处理器上执行。处理器(或处理器组)通过执行某个进程而保持忙状态,而此时其他进程处理堵塞状态。典型的调度有四种: + +### 长程调度 + +决定哪个程序可以进入系统中处理,因此它控制了系统的并发度。一旦允许进入,作业或用户程序就成为进程,并添加到供短程调度程序使用的队列中,等待调度。 + + + +### 中程调度 + +决定加入部分或全部位于内存中的进程集合。它是交换功能的一部分。典型情况下,换入(swapping-in)决定取决于管理系统并发度的需求。 + + + +### 短程调度 + +决定处理器执行哪个可运行进程。 + +长程调度程序执行的频率相对较低,并且只是大致决定是否接受新进程和接受哪个新进程。要进行交换决定,中程调度程序需要执行的稍频繁一些。短程调度程序,也成为分派程序(dispatcher),执行的最频繁,它精确的决定下次执行哪个进程。导致当前进程堵塞或抢占当前运行进程的事发生时,调用短程调度程序。这类事件包括: + +- 时钟中断 +- I/O中断 +- 操作系统调用 +- 信号(如信号量) + + + +### I/O调度 + +决定可用I/O设备处理哪个进程挂起的I/O请求 + + + +## 多处理器调度 + + + +多处理器系统分为以下几类: + +- 松耦合、分布式多处理器、集群:又一系列相对自治的系统组成,每个处理器都有自身的内存和I/O通道。 +- 专用处理器:I/O处理器是一个典型的例子。此时,有一个通用的主处理器,专用处理器由主处理器控制,并为主处理器提供服务。 +- 紧耦合多处理器:由一些列共享同一个内存并受操作系统完全控制的处理器组成。 + + + +多处理器中的调度设计三个相互关联的问题: + +- 把进程分配到处理器 + + 假设多处理器的结构是统一的,即没有哪个处理器在访问内核和I/O设备时具有物理上的特别优势,那么最简单的调度方法是把处理器视为一个资源池,并按照要求把进程分配到相应的处理器。但是这样就牵扯到静态还是动态的问题。如果一个进程从被激活到完成,一直被分配给同一个处理器,那么就需要为每个处理器维护一个专门的短程队列。这种方法的优点是调度的开销较小,因为相对于所有进程,关于处理器的分配只进行一次。静态分配的缺点就是一个处理器可能处于空闲状态,这时其队列为空,而另一个处理器却积压了许多工作。为了防止这种情况,需要使用一个公共队列。所有进程都进入一个全局队列,然后调度到任何一个可用的处理器中。这样,在一个进程的声明周期中,它可以在不同的时间于不同的处理器上执行。另一种分配策略是动态负载平衡,在该策略中,线程能在不同处理器所对应的队列之间转移。Linux采用的就是这种动态分配策略。 + + + +- 在单处理器上使用多道程序设计 + + 单处理器能够在许多进程间切换,以达到较高的利用率和更好的性能。 + +- 一个进程的实际分派 + + 与多处理器调度相关的最后一个设计问题是选择哪个进程运行。在多道程序单处理器上,与简单的先来先服务策略相比,使用优先级或基于使用历史的高级调度算法可以提高性能。考虑多处理器时,这些复杂性可能是不必要的,甚至可能起到相反的效果,而相对比较简单的方法可能会更有效,而且开销比较低。 + + + + + +### 线程调度 + +在多处理器线程调度和处理器分配的各种方案中,有四个比较突出的方法: + +- 负载分配:进程不分配到某个特定的从处理器。系统维护一个就绪线程的全局队列,每个处理器只要空闲就从队列中选择一个线程。 +- 组调度:一组相关的的线程基于一对一的原则,同时调度到一组处理器上运行。 +- 专用处理器分配:这种方法与负载分配方法正好相反,它通过把线程指定到处理器来定义隐式的调度。每个程序在其执行过程中,都分配给一组处理器,处理器的数量与程序中线程的数量相等。程序终止时,处理器返回总处理器池,以便分配给另一个程序。 +- 动态调度:在执行期间,进程中线程的数据可以改变,允许动态地改变进程中的线程数量,这就使得操作系统可以通过调整负载情况来提高利用率。 + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git a/OperatingSystem/5.IO.md b/OperatingSystem/5.IO.md new file mode 100644 index 00000000..bf043437 --- /dev/null +++ b/OperatingSystem/5.IO.md @@ -0,0 +1,42 @@ +# 5.I/O + + + +计算机系统中参与I/O的外设大体上可分为如下三类: + +- 人可读:适用于计算机用户间的交互,如打印机和终端。终端又包括显示器和键盘,以及其他一些可能的设备,如鼠标。 +- 机器可读:适用于与电子设备通信,如磁盘驱动器、USB密钥、传感器、控制器和执行器。 +- 通信:适用于与远程设备通信,如数字线路驱动器和调制解调器。 + +执行I/O的三种技术: + +- 程序控制I/O:处理器代表一个进程给I/O模块发送一个I/O命令,该进程进入忙等待,直到操作完成才能继续执行。 +- 中断驱动I/O:处理器代表进程向I/O模块发出一个I/O命令。有两种可能性:若来自进程的I/O指令是非堵塞的,则处理器继续执行发出I/O命令的进程的后续指令。若I/O指令是堵塞的,则处理器执行的下一条指令来自操作系统,它将当前的进程设置为堵塞态并调度其他进程。 +- 直接存储器访问(DMA):一个DMA模块控制内存和I/O模块之间的数据交换。为传送一块数据,处理器给DMA模块发请求,且只有在整个数据块传送结束后,它才被中断。 + + + +### 磁盘高速缓存 + + + +高速缓冲存储器(cache memory)通常指比内存小且比内存块的存储器,它位于内存和处理器之间。这种高速缓冲存储器利用局部性原理来减少平均存储器的存取时间。同样的原理也适用于磁盘存储器。磁盘高速缓存是内存中为磁盘扇区设置的一个缓冲区,它包含有磁盘中某些扇区的副本。出现对某一特定扇区的I/O请求时,首先会进行检测,以确定该扇区是否在磁盘的告诉缓存中。若在则该请求可通过这个告诉缓存来满足。若不在,则把被请求的扇区从磁盘读到磁盘高速缓存中。 + + + +磁盘的高速缓存有两个问题: + +- 当一个I/O请求从磁盘高速缓存中得到满足时,磁盘高速缓存中的数据必须传送到发送请求的进程。这可以通过在内存中把这一块数据从磁盘高速缓存传送到分配给该用户进程的存储空间中,或简单地使用一个共享内存,传送指向磁盘高速缓存中相应项的指针。 +- 置换策略。当一个新扇区被读入磁盘高速缓存时,必须换出一个已存在的块。因此这就需要一个页面置换算法。 + - 最近最少使用算法(LRU,Least Recently Used) + - 最不常使用页面置换算法(LFU, Least Frequently Used) + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/OperatingSystem/6.\346\226\207\344\273\266\347\256\241\347\220\206.md" "b/OperatingSystem/6.\346\226\207\344\273\266\347\256\241\347\220\206.md" new file mode 100644 index 00000000..50a05239 --- /dev/null +++ "b/OperatingSystem/6.\346\226\207\344\273\266\347\256\241\347\220\206.md" @@ -0,0 +1,67 @@ +# 6.文件管理 + +文件管理系统是一组系统软件,它为使用文件的用户和应用程序提供服务,包括文件访问、目录维护和访问控制。文件管理系统通常被视为一个由操作系统提供服务的系统服务,而不是操作系统的一部分,但是在任何系统中,至少有一部分文件管理功能是由操作系统执行的。 + + + +文件系统不但提供存储数据(组织为文件)的手段,而且提供一系列对文件进行操作的功能接口。典型的操作如下: + +- 创建 +- 删除 +- 打开 +- 关闭 +- 读 +- 写 + + + +### 文件系统架构 + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/file_system.png?raw=true) + + + + + +- 设备驱动(device drivers):程序直接与外围设备通信。设备驱动程序负责启动设备上的I/O操作,处理I/O请求的完成。 +- 基本文件系统或物理I/O层:是与计算机系统外部环境的基本接口。这一层处理在磁盘间或磁带系统间交换的数据块,因此它关注的是这些块在辅存和内存缓冲区中的位置,而非数据的内容或所涉及的文件结构。 +- 基本I/O管理程序(basic I/O supervisor);负责所有文件I/O的初始化和终止。在这一层,需要一定的控制结构来维护设备的输入/输出、调度和文件状态。基本I/O管理程序是操作系统的一部分。 +- 逻辑I/O(logical I/O)使用户和应用程序能够访问记录。因此基本文件系统处理的是数据块,而逻辑I/O模块处理的是文件记录。逻辑I/O提供一种通用的记录I/O的能。 + + + + + +## Android文件系统 + +Android使用了Linux中的文件管理功能。Android文件系统目录与Linux安装目录类似,只是前者有一些特有的特性。 + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_file_system.png?raw=true) + +Android文件系统目录的顶层部分: + +- system目录包含操作系统的核心部分,核心部分包括系统的二进制文件、系统库文件和配置文件。它还包含Android的基本应用,如闹钟、计算器和相机。系统映像是锁定的,文件系统只为用户提供只读权限。 +- data目录是应用程序存储文件时的首选位置。当系统中安装了一个新的应用程序时,以下这些操作都有data目录有关: + - .apk文件放置在/data/app中 + - 以应用为中心的库文件安装在/data/data/<应用名称>目录中。这个目录是特定应用程序的沙盒区域,只有该应用可以访问,其他应用不能访问。 + - 建立应用相关的文件数据库。 +- cache目录用于存储应用的临时数据。该区域存储Android系统频繁访问的数据和应用组件。清理高速缓存不会影响到个人数据,而只会简单地清理其中的已有数据,继续使用设备时,其中的数据会自动创建。 +- mnt/sdcard目录不是设备内部的内存分区,而是sd卡的分区,sd卡是一种用于android设备的非易失性的存储卡。 + + + + + + + + + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/OperatingSystem/7.\345\265\214\345\205\245\345\274\217\347\263\273\347\273\237.md" "b/OperatingSystem/7.\345\265\214\345\205\245\345\274\217\347\263\273\347\273\237.md" new file mode 100644 index 00000000..5cddf7fc --- /dev/null +++ "b/OperatingSystem/7.\345\265\214\345\205\245\345\274\217\347\263\273\347\273\237.md" @@ -0,0 +1,27 @@ +# 7.嵌入式系统 + + + +为完成某个特定功能而设计的,或许有附加机制或其他部分的计算机硬件和软件结合体。在许多情况下,嵌入式系统是一个更大系统或产品中的一部分,如汽车中的防抱死系统。 + + + +## 嵌入式Linux + +嵌入式Linux是指运行在嵌入式系统中的Linux。嵌入式Linux是Linux的一个版本,是基于嵌入式设备的大小和硬件限制而定制的,它同时包括一些软件包,用于支持设备商运行的服务和应用。因此嵌入式Linux的内核比普通Linux的内核要小得多。 + +台式机/服务器Linux与嵌入式Linux的一个关键区别是: 台式机/服务器软件通常是在运行平台上编译的,而嵌入式Linux通常在一个平台上编译,但运行于另一个平台,后者称为交叉编译。 + + + +Android是基于Linux内核的一个嵌入式系统,因此我们可以认为Android是嵌入式Linux的一个例子。但是,很多嵌入式Linux开发人员不认为Android系统是嵌入式Linux的实例。他们认为,传统的嵌入式系统拥有固定的功能,而且在出厂时就已确定。Adnroid能支持各种的应用,因此要比普通平台性操作系统强大得多。而且,Android是垂直一体化的系统,包括针对Linux内核的特定修改。 + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/OperatingSystem/8.\350\231\232\346\213\237\346\234\272.md" "b/OperatingSystem/8.\350\231\232\346\213\237\346\234\272.md" new file mode 100644 index 00000000..73d02145 --- /dev/null +++ "b/OperatingSystem/8.\350\231\232\346\213\237\346\234\272.md" @@ -0,0 +1,134 @@ +# 8.虚拟机 + +通常而言,应用程序直接运行在PC或服务器的操作系统上。每台PC或服务器在同一时间只运行一个操作系统。因此,应用程序供应商需要为每个操作系统或平台重写应用程序的部分代码,才能使应用能够得到系统支持的运行。要支持多种操作系统,应用程序供应商需要创建、管理和维护多种硬件与操作系统基础设施,这一过程需要耗费昂贵的代价和大量的资源。处理这个问题的有效策略之一称为虚拟化(virtualization),虚拟化技术可使一台PC或服务器同时运行多个操作系统或一个系统的多个会话。一台运行虚拟化软件的机器能在同一平台上运行大量的应用程序,包括那些运行在不同操作系统上的应用程序。实际上,主机操作系统能支持多个虚拟机(virtual machines),每个虚拟机都有特定操作系统的特性。 + +启用虚拟化的解决方案是虚拟机监视器(VMM),现在通常称为虚拟机管理程序(Hypervisor)。该软件介于硬件和虚拟机之间,以资源代理的形式存在。简而言之,它使多个虚拟机安全地共存于一台物理服务器主机并共享主机的资源。 + + + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/virtual_1.png?raw=true) + + + +## Java虚拟机 + + + +尽管Java虚拟机(JVM)用“虚拟机”作为其名称的一部分,但其实现和用途与我们前面所讲的模型不同。虚拟机管理程序支持在主机上运行一个或多个虚拟机。这些虚拟机独立的处理工作负载,支持操作系统和应用,且在它们自身看来,访问一系列提供计算、存储和输入/输出的资源。Java虚拟机的目的是,无须更改任何Java代码就可在任意硬件平台的任意操作系统上,提供运行时空间。两种模型的目的都是通过使用某种程度的抽象化来实现平台无关性。 + +JVM可描述为一个抽象的计算设备,它包含指令集、一个PC(程序计数器)寄存器、一个用来保存变量和结果的栈、一个保存运行时数据和垃圾手机的堆、一个存储代码和常量的方法区。 + +JVM支持多个线程,每个线程都有自己的寄存器和堆栈区,且所有线程共享栈和方法区。 + + + +## Android虚拟机 + + + +Android平台的虚拟机称为Dalvik,Dalvik VM(DVM)执行格式为Dalvik Executable(.dex格式)的文件,即为高效存储和内存映射执行而优化的格式。DVM可以运行由Java编译器编译的类,该编译器以用“dx”工具转换为本地格式。 + +虚拟机运行在Linux内核的顶部,它依赖于底层的功能(如线程和底层的内存管理)。Dalvik核心类库的目的是,为那些使用标准Java编程的人员提供熟悉的开发环境,但它是专门为满足小型移动设备的需要而设计的。 + + + +每个Android应用程序都运行在自己的进程中,有自己的Dalvik运行实例。Dalvik是可以在一台设备上高效执行多个副本的虚拟机。 + + + + + +### dex文件系统 + +DVM运行Java语言的应用和代码。标准的Java编译器将源代码(写在文本文件中)转换为字节码,然后将字节码翻译成DVM虚拟机可读和可用的.dex文件。本质上,类文件被转换为.dex文件(像Java虚拟机中的jar文件),然后在DVM上读取和执行。类文件中的重复数据在.dex文件中只包含一次,以节省空间开销。在安装时,这个可执行文件还可根据移动设备进一步修改和优化。 + + + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/java_vs_dalvik.png?raw=true) + +上图中的.jar文件的布局,它包含一个或多个类文件。这些类文件聚合为一个.dex文件,并存储为一个android安装包文件(.apk)。所有类文件中的不同常量池集中为单个常量池,在.dex文件中组织为常量类型。允许类的常量池共享,因此可使常量值的重复减至最低。类似的,类文件中的类、域、方法、属性也在.dex文件中集中到一起。 + + + + + +## Android进程结构 + +如同传统的Linux系统一样,Android的第一个用户空间进程是init,它是所有其他进程的根。然而,Android的init启动的守护进程是不同的,这些守护进程更多的聚焦于底层细节(管理文件系统和硬件访问),而不是高层用户设施,例如调度定时任务。Android还有一层额外的进程,它们运行Dalvik的Java语言环境,负责执行系统中所有以Java实现的部分。 + + + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_process.png?raw=true) + + + +如上图,首先是init进程,它产生了一些底层守护进程。其中一个守护进程是zygote,它是高级Java语言进程的根。Android的init不以传统的方法运行shell,因为典型的Android设备没有本地控制台用于shell访问。作为替代,系统进程adbd监听请求shell访问的远程连接(例如通过USB),按要求为它们创建shell进程。因为Android大部分是用Java语言编写的,所以zygote守护进程以及由它启动的进程是系统的中心。由zygote启动的第一个进程称为system_server,它包含全部核心操作服务,其关键部分是电源管理、包管理、窗口管理和活动管理。 + +其他进程在需要的时候由zygote创建。这些进程中有一些是“持久的”进程,它们是基本操作系统的组成部分,例如phone进程中的电话栈,它必须保持始终运行。另外的应用程序进程将在系统运行的过程中按需创建和终止。 + +应用程序通过调用操作系统提供的库与操作系统进行交互,这些库合起来构成Android框架(Android framework)。这些库中有一些可以在进程内部执行其工作,但是许多库需要与其他进程执行进程间通信,作者通常是在system_server进程中提供服务的。 + + + +### Zygote + +Zygote是在启动时就运行在DVM上的一个进程。每当出现创建进程的请求时,Zygote就会产生一个新的DVM虚拟机。Zygote通过在内存中尽可能多的共享内容来最小化产生一个新DVM所消耗的时间。通常而言,许多应用程序都会使用核心库的类和相应的堆结构,而这些内容都是只读的。也就是说,被大部分应用程序使用的这些共享数据和类都是只读而不能改变的。因此,当Zygote加载时,就预加载和初始化了应用程序运行时可能用到的Java核心库和资源。当Zygote创建一个新的DVM时,这部分类不会被分配到新内存。Zygote只是简单地把子进程的这些内存页映射到父进程的相应位置。 + +实际上,几乎不需要更多的映射页。如果一个类被一个子进程自己的DVM改写,那么Zygote会将受影响的内存复制到子进程中。这种即写即复制的行为在使得最大化共享内存的同时,还能保证应用程序间不会相互影响,并在跨应用程序和进程的边界时保证安全性。 + + + +## Android进程模型 + + + +Linux的传统进程模型是用fork指令来创建新进程,然后用exec指令使用待运行的源码初始化该进程并开始执行。shell负责实现进程执行、创建新进程、执行所需的进程来运行shell指令。当指令结束时,进程被从Linux中移除。 + +Android使用的进程有些不同。活动管理器是Android负责正在运行的应用程序的管理的一部分。活动管理器协调新应用程序进程的启动,决定哪些应用程序能在其中运行,哪些已不再需要。 + + + +### 启动进程 + +为了启动新进程,活动管理器需要与zygote通信。活动管理器首先开始,它创建一个与zygote相连的专用接口,通过接口发送一条指令,表示它需要启动一个进程。这条指令主要描述需要创建的沙箱、新进程运行所需要的UID以及需要遵守的安全性制约。zygote需要作为根来运行:创建新进程时,它合理配置运行所需的UID,最终下放权限,将进程改为该UID。 + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_start_process.png?raw=true) + +上图展示了一个新进程中启动活动的流程: + +1. 某个现有进程(如应用程序启动器)调用活动管理器,发出意图,描述它想要启动的新活动。 +2. 活动管理器要求封装管理器将这个意图解析为一个明确的组件。 +3. 活动管理器判断这个应用程序的进程并未正在运行,然后向zygote请求一个具有合适UID的新锦成。 +4. zygote进行一次fork指令,克隆自己来创造一个新进程,下方权限并配置新进程的UID和沙箱,初始化该进程的Dalvik,使得Java runtime开始完全执行。例如,它需要在fork后启动垃圾收集等线程。 +5. 新进程如今是一个zygote的克隆,并运行着完全配置好的Java环境。它回调活动管理器,询问后者“我该做什么”。 +6. 活动管理器返回即将启动的应用程序的完整信息,如源码位置等。 +7. 新进程读取应用程序的源码,开始运行。 +8. 活动管理器将所有即将进行的操作发送给新进程,在此处为“启动活动X”。 +9. 新进程收到指令,启动活动,实体化合适的Java类并执行。 + +注意,当活动启动时,应用程序的进程可能正在运行了。在这种情况下,活动管理器会直接跳转到末尾,向该进程发送一条新指令,让它实体化并执行合适的组件。如果合适,这会导致一个额外的活动实例在应用程序中运行。 + + + +### 进程声明周期 + +活动管理器也负责判断何时进程不再被需要。活动管理器记录一个进程中运行的所有活动、接收器、服务以及内容提供其,据此可判断该进程的重要程度。 + +Android内核中的内存溢出强制结束指令是使用一个进程的oom_adj进行严格排序,决定哪个进程需要优先强制结束。活动管理器负责基于每个进程的状态,通过将其归类于几个主要用途,从而合理设定器oom_adj。/proc//oom_adj: + +- 取值是-17到+15,取值越高,越容易被干掉。如果是-17,则表示不能被kill +- 该设置参数的存在是为了和旧版本的内核兼容 + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_process_adj.png?raw=true) + +让RAM内存不足时,系统已经完成了进程的配置,使得内存溢出强制结束命令优先中止缓存(cache)类型的进程,尝试重新取得足够的所需内存,随后中止界面(home)类别、服务(service)类别,以此类推。在同一个oom_adj水平中,它将优先中止内存占用较大的进程。 + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! From 357e4fed3bae520c57beffca1d9947f4c47fee5e Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 26 Nov 2020 09:36:25 +0800 Subject: [PATCH 032/213] update README --- ...73\347\273\237\347\256\200\344\273\213.md" | 2 +- ...13\344\270\216\347\272\277\347\250\213.md" | 2 +- OperatingSystem/{5.IO.md => 5.I:O.md} | 0 README.md | 28 +++++++++++++++---- 4 files changed, 25 insertions(+), 7 deletions(-) rename "OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237.md" => "OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" (99%) rename "OperatingSystem/2.\350\277\233\347\250\213.md" => "OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" (99%) rename OperatingSystem/{5.IO.md => 5.I:O.md} (100%) diff --git "a/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237.md" "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" similarity index 99% rename from "OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237.md" rename to "OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" index 00696a2d..d1351546 100644 --- "a/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237.md" +++ "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" @@ -1,6 +1,6 @@ -# 1.操作系统 +# 1.操作系统简介 diff --git "a/OperatingSystem/2.\350\277\233\347\250\213.md" "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" similarity index 99% rename from "OperatingSystem/2.\350\277\233\347\250\213.md" rename to "OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" index cad290fc..5f19d32d 100644 --- "a/OperatingSystem/2.\350\277\233\347\250\213.md" +++ "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" @@ -1,4 +1,4 @@ -# 1.进程 +# 2.进程与线程 diff --git a/OperatingSystem/5.IO.md b/OperatingSystem/5.I:O.md similarity index 100% rename from OperatingSystem/5.IO.md rename to OperatingSystem/5.I:O.md diff --git a/README.md b/README.md index 316ffbc9..41171f32 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,6 @@ Android学习笔记 - [史上最适合Android开发者学习的Go语言教程](https://github.com/CharonChui/GolangStudyNote) - [史上最适合Android开发者学习的iOS开发教程](https://github.com/CharonChui/iOSStudyNote) - - [源码解析][43] - [自定义View详解][1] - [Activity界面绘制过程详解][2] @@ -32,7 +31,6 @@ Android学习笔记 - [Volley源码分析][15] - [Retrofit详解(上)][16] - [Retrofit详解(下)][17] - - [Dagger2][199] - [1.Dagger2简介(一).md][200] - [2.Dagger2入门demo(二).md][201] @@ -43,7 +41,6 @@ Android学习笔记 - [7.Dagger2之dagger-android(七).md][206] - [8.Dagger2与MVP(八).md][207] - [9.Dagger2原理分析(九).md][212] - - [音视频开发][44] - [搭建nginx+rtmp服务器][18] - [视频播放相关内容总结][19] @@ -98,6 +95,18 @@ Android学习笔记 - [11.OpenGL ES滤镜][242] - [弹幕][243] - [Android弹幕实现][244] +- [操作系统][263] + - [1.操作系统简介][264] + - [2.进程与线程][265] + - [3.内存管理][266] + - [4.调度][267] + - [5.I/O][268] + - [6.文件管理][269] + - [7.嵌入式系统][270] + - [8.虚拟机][271] +- [架构设计][272] + - [1.架构简介][273] + - [图片加载][45] - [Glide简介(上)][25] - [Glide简介(下)][26] @@ -556,8 +565,17 @@ Android学习笔记 [260]:https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E8%A7%86%E9%A2%91%E7%BC%96%E7%A0%81/H265.md "H265" [261]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/P2P%E6%8A%80%E6%9C%AF/P2P.md "P2P" [262]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/P2P%E6%8A%80%E6%9C%AF/P2P%E5%8E%9F%E7%90%86_NAT%E7%A9%BF%E9%80%8F.md "P2P原理_NAT穿透" - - +[263]: https://github.com/CharonChui/AndroidNote/tree/master/OperatingSystem "操作系统" +[264]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/1.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F.md "1.操作系统简介" +[265]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/2.%E8%BF%9B%E7%A8%8B.md "2.进程和线程" +[266]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/3.%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86.md "3.内存管理" +[267]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/4.%E8%B0%83%E5%BA%A6.md "4.调度" +[268]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/5.IO.md "5.I/O" +[269]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/6.%E6%96%87%E4%BB%B6%E7%AE%A1%E7%90%86.md "6.文件管理" +[270]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/7.%E5%B5%8C%E5%85%A5%E5%BC%8F%E7%B3%BB%E7%BB%9F.md "7.嵌入式系统" +[271]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/8.%E8%99%9A%E6%8B%9F%E6%9C%BA.md "8.虚拟机" +[272]: https://github.com/CharonChui/AndroidNote/tree/master/Architect "架构设计" +[273]: https://github.com/CharonChui/AndroidNote/blob/master/Architect/1.%E6%9E%B6%E6%9E%84%E7%AE%80%E4%BB%8B.md "1.架构简介" Developed By From a13218a2a97024f7288ee69e52a41a4c2342f579 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 26 Nov 2020 09:40:53 +0800 Subject: [PATCH 033/213] update README --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 41171f32..91a59d53 100644 --- a/README.md +++ b/README.md @@ -566,11 +566,13 @@ Android学习笔记 [261]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/P2P%E6%8A%80%E6%9C%AF/P2P.md "P2P" [262]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/P2P%E6%8A%80%E6%9C%AF/P2P%E5%8E%9F%E7%90%86_NAT%E7%A9%BF%E9%80%8F.md "P2P原理_NAT穿透" [263]: https://github.com/CharonChui/AndroidNote/tree/master/OperatingSystem "操作系统" -[264]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/1.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F.md "1.操作系统简介" -[265]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/2.%E8%BF%9B%E7%A8%8B.md "2.进程和线程" +[264]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/1.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%AE%80%E4%BB%8B.md "1.操作系统简介" +[265]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/2.%E8%BF%9B%E7%A8%8B%E4%B8%8E%E7%BA%BF%E7%A8%8B.md "2.进程和线程" [266]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/3.%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86.md "3.内存管理" [267]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/4.%E8%B0%83%E5%BA%A6.md "4.调度" -[268]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/5.IO.md "5.I/O" + +[268]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/5.I:O.md "5.I/O" + [269]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/6.%E6%96%87%E4%BB%B6%E7%AE%A1%E7%90%86.md "6.文件管理" [270]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/7.%E5%B5%8C%E5%85%A5%E5%BC%8F%E7%B3%BB%E7%BB%9F.md "7.嵌入式系统" [271]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/8.%E8%99%9A%E6%8B%9F%E6%9C%BA.md "8.虚拟机" From 9d6f06a3625eebf1c80612e7da8d02e9cba8bc28 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 26 Nov 2020 10:19:46 +0800 Subject: [PATCH 034/213] update README --- ...73\347\273\237\347\256\200\344\273\213.md" | 70 ++++++++++--------- ...13\344\270\216\347\272\277\347\250\213.md" | 2 + 2 files changed, 38 insertions(+), 34 deletions(-) diff --git "a/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" index d1351546..b6dd93ea 100644 --- "a/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" +++ "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" @@ -35,7 +35,7 @@ #### 内存管理(Memory management) -内存是计算机很重要的一个资源,因为程序只有被加载到内存中才可以运行,此外 CPU 所需要的指令与数据也都是来自内存的。内存的并不是无限制的,它受限于硬件和寻址位数。但是现代操作系统会让每一个进程都觉得自己在独占整个内存,这就是虚拟内存技术。值得注意的是,这里的虚拟内存与 swap 这种虚拟内存是不一样的,虽然两个都是成为虚拟内存,但是完全是两个不同方向的技术。 +内存是计算机很重要的一个资源,因为程序只有被加载到内存中才可以运行,此外 CPU 所需要的指令与数据也都是来自内存的。内存的并不是无限制的,它受限于硬件和寻址位数。但是现代操作系统会让每一个进程都觉得自己在独占整个内存,这就是虚拟内存技术。值得注意的是,这里的虚拟内存与 swap 这种虚拟内存是不一样的,虽然两个都是称为虚拟内存,但是完全是两个不同方向的技术。 进程的运行需要分配内存,内存分配的快慢都与内存管理方式有着巨大的影响。两个不同进程对应的内存区域是不能相互访问的,操作系统必须得提供这样的保证,否则很容易出问题,比如:运行着的 dota 游戏如果可以被另外一个进程访问它的内存区域的话,那就可以直接将内存区域中的某个数值进行修改,比如将游戏中的玩家生命值变为无限,这样对手怎么打都打不死自己的英雄。例如前段时间火热的吃鸡游戏,外挂软件可以让角色在决赛圈外进行锁血,这个就是游戏内存被修改的最好示例。当然这是通过比较专业的手段来绕过操作系统的限制,这也从另外一个方面来说明,其实现在的操作系统安全性也是有很大提升空间的。 进程退出销毁时,内存的回收也是很重要的,否则很容易就会产生内存溢出,占着茅坑不拉屎,导致其他的进程都憋死。 @@ -55,7 +55,7 @@ 我们常见的网络通信场景有:微信聊天、浏览网页、玩网络游戏等,可以说现在的操作系统如果不能上网感觉就没了灵魂。网络通信是个很复杂的过程,连很多专业的程序猿都说不清楚当他上网时网页是如何显示出来的,更别说整个网络的拓扑结构了。 -整个网络通信其实是一套约定好的通信协议,很多人第一次听说协议时觉得很高级,其实没什么高级的,简单的说,类似于我们军训时当教官喊立正我们必须得做出相应动作一样,一个指令对应一个动作。由于网络太复杂了,某位哲人说过如果某样东西太复杂可以通过分层来解决,于是网络分了 5 层。有人也许会说是 7 层,那是 OSI 标准,在实际应用中一般都是 5 层。由于网络这块内容实在是太复杂,后面我会另开一个专栏进行讲解。 +整个网络通信其实是一套约定好的通信协议,很多人第一次听说协议时觉得很高级,其实没什么高级的,简单的说,类似于我们军训时当教官喊立正我们必须得做出相应动作一样,一个指令对应一个动作。由于网络太复杂了,某位哲人说过如果某样东西太复杂可以通过分层来解决,于是网络分了 5 层。有人也许会说是 7 层,那是 OSI 标准,在实际应用中一般都是 5 层。 @@ -74,16 +74,16 @@ -计算机由处理器、存储器和输入\输出部件组成,每类部件都有一个或多个模块。这些部件以某种方式互连,以实现计算机执行程序的主要功能。因此,计算机有4个主要的结构化部件: +计算机由处理器、存储器和输入/输出部件组成,每类部件都有一个或多个模块。这些部件以某种方式互连,以实现计算机执行程序的主要功能。因此,计算机有4个主要的结构化部件: - 处理器(Processor):控制计算机的操作,执行数据处理功能。只有一个处理器时,它通常指中央处理器(CPU) -- 内存(Main memory):存储数据和程序。此类存储器通常是易失性的,即当计算机关机时,存储器的内容会丢失。相对于此的是磁盘存储器,当计算机关机时,它的内容不会丢失。内存通常也成为实存储器(real memory)或主存储器(primary memory)。 -- 输入\输出模块(I/O modules):在计算机和外部环境之间移动数据。外部环境由各种外部设备组成,包括辅助存储器设备(如硬盘)、通信设备和终端。 -- 系统总线(System bus)在处理器、内存和输入\输出模块间提供通信的设施。 +- 内存(Main memory):存储数据和程序。此类存储器通常是易失性的,即当计算机关机时,存储器的内容会丢失。相对于此的是磁盘存储器,当计算机关机时,它的内容不会丢失。内存通常也称为实存储器(real memory)或主存储器(primary memory)。 +- 输入/输出模块(I/O modules):在计算机和外部环境之间移动数据。外部环境由各种外部设备组成,包括辅助存储器设备(如硬盘)、通信设备和终端。 +- 系统总线(System bus):在处理器、内存和输入/输出模块间提供通信的设施。 -处理器的一种功能是与存储器叫唤数据。为此,它通常使用两个内部寄存器: +处理器的一种功能是与存储器交换数据。为此,它通常使用两个内部寄存器: - 存储器地址寄存器(Memory Address Register, MAR),用于确定下一次读/写的存储器地址。 - 存储器缓冲寄存器(Memory Buffer Register,MBR),存放要写入存储器的数据或从存储器中读取的数据。 @@ -100,12 +100,14 @@ ### 计算机打开电源后的执行 -在每台计算机上有一块双亲板(在政治因素影响到计算机产业之前,它们曾称为“母版”)。在双亲板上有一个称为基本输入输出系统(Basic Input Output System, BIOS)的程序。在BIOS内有底层I/O软件,包括读键盘、写屏幕、进行磁盘I/O以及其他过程。在计算机启动时,BIOS开始运行。它首先检查所安装的RAM数量、键盘和其他基本设备是否已安装并正常相应。接着,它开始扫描PCIe和PCI总线并找出连在上面的所有设备。即插即用设备也被记录下来。如果现有的设备和系统上一次启动时的设备不同,则新的设备将被配置。然后BIOS通过尝试存储在CMOS存储器中的设备清单决定启动设备。用户可以在系统刚启动之后进入一个BIOS配置程序,对设备清单进行修改。典型的,如果存在CD-ROM(有时是USB),则系统试图从中启动(之前重装系统就是这么操作的)。如果失败,系统将从硬盘启动。启动设备上的第一个扇区被读入内存并执行。这个扇区中包含一个对保存在启动扇区末尾的分区表检查的程序,以确定哪个分区是活动的。然后,从该分区读入第二个启动装载模块。来自活动分区的这个装载模块被读入操作系统,并启动之。 +在每台计算机上有一块双亲板(在政治因素影响到计算机产业之前,它们曾称为“母版”)。在双亲板上有一个称为基本输入输出系统(Basic Input Output System, BIOS)的程序。在BIOS内有底层I/O软件,包括读键盘、写屏幕、进行磁盘I/O以及其他过程。在计算机启动时,BIOS开始运行。它首先检查所安装的RAM数量、键盘和其他基本设备是否已安装并正常响应。接着,它开始扫描PCIe和PCI总线并找出连在上面的所有设备。即插即用设备也被记录下来。如果现有的设备和系统上一次启动时的设备不同,则新的设备将被配置。然后BIOS通过尝试存储在CMOS存储器中的设备清单决定启动设备。用户可以在系统刚启动之后进入一个BIOS配置程序,对设备清单进行修改。典型的,如果存在CD-ROM(有时是USB),则系统试图从中启动(之前重装系统就是这么操作的)。如果失败,系统将从硬盘启动。启动设备上的第一个扇区被读入内存并执行。这个扇区中包含一个对保存在启动扇区末尾的分区表检查的程序,以确定哪个分区是活动的。然后,从该分区读入第二个启动装载模块。来自活动分区的这个装载模块被读入操作系统,并启动之。 然后,操作系统询问BIOS,以获得配置信息。对于每种设备,系统检查对应的设备驱动程序是否存在。如果没有,系统要求用户插入含有该驱动程序的CD-ROM(由设备供应商提供)或者从网络上下载驱动程序。一旦有了全部的设备驱动程序,操作系统就将它们调入内核。然后初始化有关表格,创建需要的任何背景进程,并在每个终端上启动登陆程序或GUI。 +如果是从普通硬盘启动,可简单列举为如下步骤: + 1. X86 PC开机时CPU处于实模式,开机时会将cs=0xffff,ip=0x0000 -2. 寻址0xFFFF0(ROM BIOS营社区) +2. 寻址0xFFFF0(ROM BIOS映射区) 3. 检查RAM、键盘、显示器、磁盘、主板等硬件 4. 将磁盘0磁道0扇区(操作系统引导扇区)读入0x7c00处 5. 设置 cs=0x07c0,ip=0x0000开始执行 @@ -114,9 +116,7 @@ ## CPU(Central Processing Unit) -CPU 是计算机的大脑,它主要和内存进行交互,从内存中提取指令并执行它。CPU(中央处理器)是一块超大规模的集成电路Integrated Circuit),是信息处理、程序运行的最终执行单元。其功能主要是解释计算机指令以及处理计算机软件中的数据。从功能方面来看,CPU的内部主要由寄存器,控制器,运算器构成,各部分之间由电流信号相互连通。其中运算器负责算术运算和逻辑运算,控制器负责计算指令的解析,产生各种控制指令,寄存器组用来临时存放参加运算的数据和计算的中间结果。CPU计算结果最终需要写到内存中,内存的存取速度远低于CPU,为提升数据交换速率,CPU内部一般还集成了高速缓存(CACHE),其中缓存分为一级缓存和二级缓存,一级缓存和CPU速率相当,二级缓存次之。其实在现在一些CPU中,一级缓存也会分为一级数据缓存和一级指令缓存。 - -由于访问内存获取执行或数据要比执行指令花费的时间长,因此所有的 CPU 内部都会包含一些`寄存器`来保存关键变量和临时结果。因此,在指令集中通常会有一些指令用于把关键字从内存中加载到寄存器中,以及把关键字从寄存器存入到内存中。 +CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从内存中提取指令并执行它。它是一块超大规模的集成电路Integrated Circuit),是信息处理、程序运行的最终执行单元。其功能主要是解释计算机指令以及处理计算机软件中的数据。由于访问内存获取执行或数据要比执行指令花费的时间长,因此所有的 CPU 内部都会包含一些寄存器来保存关键变量和临时结果。因此,在指令集中通常会有一些指令用于把关键字从内存中加载到寄存器中,以及把关键字从寄存器存入到内存中。从功能方面来看,CPU的内部主要由寄存器,控制器,运算器构成,各部分之间由电流信号相互连通。其中运算器负责算术运算和逻辑运算,控制器负责计算指令的解析,产生各种控制指令,寄存器组用来临时存放参加运算的数据和计算的中间结果。CPU计算结果最终需要写到内存中,内存的存取速度远低于CPU,为提升数据交换速率,CPU内部一般还集成了高速缓存(CACHE),其中缓存分为一级缓存和二级缓存,一级缓存和CPU速率相当,二级缓存次之。 @@ -124,27 +124,27 @@ CPU 是计算机的大脑,它主要和内存进行交互,从内存中提取 使用高级语言编写的程序会在编译后转化成机器语言,然后通过CPU内部的寄存器来处理。不同类型的CPU,其内部寄存器的数量,种类以及寄存器存储的数值范围都是不同的。根据功能的不同,我们可以将寄存器大致划分为八类。 -**累加寄存器:**存储执行运算的数据和运算后的数据。 +***累加寄存器***:存储执行运算的数据和运算后的数据。 -**标志寄存器:**存储运算处理后的CPU的状态。 +***标志寄存器***:存储运算处理后的CPU的状态。 -**程序计数器:**存储下一条指令所在内存的地址。 +***程序计数器***:存储下一条指令所在内存的地址。 -**基址寄存器:**存储数据内存的起始地址。 +***基址寄存器***:存储数据内存的起始地址。 -**变址寄存器:**存储基址寄存器的相对地址。 +***变址寄存器***:存储基址寄存器的相对地址。 -**通用寄存器:**存储任意数据。 +***通用寄存器***:存储任意数据。 -**指令寄存器:**存储指令。CPU内部使用,程序员无法通过程序对该寄存器进行读写操作。 +***指令寄存器***:存储指令。CPU内部使用,程序员无法通过程序对该寄存器进行读写操作。 -**栈寄存器:**存储栈区域的起始地址。 +***栈寄存器***:存储栈区域的起始地址。 其中,程序计数器,累加寄存器,标志寄存器,指令寄存器和栈寄存器都只有一个,其他的寄存器一般有多个。 -**计算机执行的原理是: 取指执行 ** +***计算机执行的原理是: 取指执行*** 处理器执行的程序是由一组保存在存储器中的指令组成的。最简单的指令处理包括两步: 处理器从存储器中一次读(取)一条指令,然后执行每条指令。程序执行是由不断重复的取指令和执行指令的过程组成的。指令执行可能涉及很多操作,具体取决于指令本身。 @@ -163,11 +163,11 @@ CPU 是计算机的大脑,它主要和内存进行交互,从内存中提取 几乎所有的冯·诺伊曼型计算机的CPU,其工作都可以分为5个阶段:**取指令、指令译码、执行指令、访存取数、结果写回**。 -- `取指令`阶段是将内存中的指令读取到 CPU 中寄存器的过程,程序寄存器用于存储下一条指令所在的地址 -- `指令译码`阶段,在取指令完成后,立马进入指令译码阶段,在指令译码阶段,指令译码器按照预定的指令格式,对取回的指令进行拆分和解释,识别区分出不同的指令类别以及各种获取操作数的方法。 -- `执行指令`阶段,译码完成后,就需要执行这一条指令了,此阶段的任务是完成指令所规定的各种操作,具体实现指令的功能。 -- `访问取数`阶段,根据指令的需要,有可能需要从内存中提取数据,此阶段的任务是:根据指令地址码,得到操作数在主存中的地址,并从主存中读取该操作数用于运算。 -- `结果写回`阶段,作为最后一个阶段,结果写回(Write Back,WB)阶段把执行指令阶段的运行结果数据“写回”到某种存储形式:结果数据经常被写到CPU的内部寄存器中,以便被后续的指令快速地存取; +- 取指令阶段是将内存中的指令读取到 CPU 中寄存器的过程,程序寄存器用于存储下一条指令所在的地址 +- 指令译码阶段,在取指令完成后,立马进入指令译码阶段,在指令译码阶段,指令译码器按照预定的指令格式,对取回的指令进行拆分和解释,识别区分出不同的指令类别以及各种获取操作数的方法。 +- 执行指令阶段,译码完成后,就需要执行这一条指令了,此阶段的任务是完成指令所规定的各种操作,具体实现指令的功能。 +- 访问取数阶段,根据指令的需要,有可能需要从内存中提取数据,此阶段的任务是:根据指令地址码,得到操作数在主存中的地址,并从主存中读取该操作数用于运算。 +- 结果写回阶段,作为最后一个阶段,结果写回(Write Back,WB)阶段把执行指令阶段的运行结果数据“写回”到某种存储形式:结果数据经常被写到CPU的内部寄存器中,以便被后续的指令快速地存取; @@ -185,7 +185,7 @@ CPU 是计算机的大脑,它主要和内存进行交互,从内存中提取 于是出现了多级存储器组织结构: -- 寄存器: 最快、最小和最贵的存储器类型由位于处理器内部的寄存器组成。它们用预CPU相同的材料制成,所以和CPU一样快。显然,访问它们是没有时延的。其典型的存储容量是,在32位CPU中为32*32位,而在64位CPU中为64*64位。在这两种情况下,其存储容量都小于1KB。典型情况下,一个处理器包含多个寄存器,某些处理器包含上百个寄存器。 +- 寄存器: 最快、最小和最贵的存储器类型由位于处理器内部的寄存器组成。它们用与CPU相同的材料制成,所以和CPU一样快。显然,访问它们是没有时延的。其典型的存储容量是,在32位CPU中为32x32位,而在64位CPU中为64x64位。在这两种情况下,其存储容量都小于1KB。典型情况下,一个处理器包含多个寄存器,某些处理器包含上百个寄存器。 - 高速缓存,它多数由硬件控制。 @@ -197,9 +197,11 @@ CPU 是计算机的大脑,它主要和内存进行交互,从内存中提取 ![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/changpianji.jpg?raw=true) - 有时,还有一些实际上不是磁盘的磁盘,比如固态硬盘(Solid State Disk,SSD)。固态硬盘并没有可以移动的部分,外形也不像唱片那样,并且数据是存储在存储器(闪存)中的。与磁盘唯一的相似之处就是它也存储了大量即使在电源关闭时也不会丢失的数据。 - -这里要说一下闪存(flash memory),在便捷式电子设备中,闪存通常作为存储媒介。闪存是数吗相机中的胶卷。是便捷式音乐播放器的磁盘。这仅仅是闪存用途的两项。闪存在速度上介于RAM和磁盘之间。另外,与磁盘存储器不同的是,如果闪存擦除次数过多,就被磨损了。 + 有时,还有一些实际上不是磁盘的磁盘,比如固态硬盘(Solid State Disk,SSD)。固态硬盘并没有可以移动的部分,外形也不像唱片那样,并且数据是存储在存储器(闪存)中的。与磁盘唯一的相似之处就是它也存储了大量即使在电源关闭时也不会丢失的数据。 + + 这里要说一下闪存(flash memory),闪存是一种基于硅芯片的存储介质,可以用电写入或擦除。在便捷式电子设备中,闪存通常作为存储媒介。闪存是数吗相机中的胶卷。是便捷式音乐播放器的磁盘。这仅仅是闪存用途的两项。闪存在速度上介于RAM和磁盘之间。另外,与磁盘存储器不同的是,如果闪存擦除次数过多,就被磨损了。 + + 那闪存和固态硬盘有什么区别?固态硬盘也是将数据存储在闪存中。在存储行业中使用的最简单的类比之一是闪存就像鸡蛋,而SSD硬盘就像煎蛋卷一样。煎蛋卷主要是由鸡蛋制作的,而SSD硬盘主要由闪存支制成的。 @@ -223,11 +225,11 @@ CPU 是计算机的大脑,它主要和内存进行交互,从内存中提取 - RAM(Random-access memory) - 随机存取存储器,一般使用动态半导体存储器件(DRAM),对于CPU来说,RAM是主要存放数据和程序的地方,所以也叫做“主存”,因为CPU工作的速度比RAM的读写速度快,所以CPU读写RAM时需要花费时间等待,这样就使CPU的工作速度下降。人们为了提高CPU读写程序和数据的速度,在RAM和CPU之间增加了高速缓存(Cache)部件。Cache的内容是随机存储器(RAM)中部分存储单元内容的副本 + 随机存取存储器,一般使用动态半导体存储器件(DRAM),对于CPU来说,RAM是主要存放数据和程序的地方,所以也叫做“主存”。 - ROM(Read-Only Memory) - 只读存储器,出厂时其内容由厂家用掩膜技术写好,只可读出,但无法改写。信息已固化在存储器中,一般用于存放系统程序BIOS和用于微程序,断电也没有关系,放ROM的数据一辈子都不会变 + 只读存储器,出厂时其内容由厂家用掩膜技术写好,只可读出,但无法改写。信息已固化在存储器中,一般用于存放系统程序BIOS和用于微程序,断电也没有关系,放ROM的数据一辈子都不会变。 @@ -241,7 +243,7 @@ CPU 是计算机的大脑,它主要和内存进行交互,从内存中提取 -缓存基本上都是采用SRAM存储器,SRAM是英文Static RAM的缩写,它是一种具有静态存取功能的存储器,不需要刷新电路即能保存它内部存储的数据。不像DRAM内存那样需要刷新电路,每隔一段时间,固定要对DRAM刷新充电一次,否则内部的数据即会消失,因此SRAM具有较高的性能,但是SRAM也有它的缺点,即它的集成度较低,相同容量的DRAM内存可以设计为较小的体积,但是SRAM却需要很大的体积,这也是不能将缓存容量做得太大的重要原因。它的特点归纳如下:优点是节能、速率快、不必配合内存刷新电路、可提高整体的工作效率,缺点是集成度低、相同的容量体积较大、而且价格较高,只能少量用于关键性系统以提高效率。 +缓存基本上都是采用SRAM存储器,它是一种具有静态存取功能的存储器,不需要刷新电路即能保存它内部存储的数据。不像DRAM内存那样需要刷新电路,每隔一段时间,固定要对DRAM刷新充电一次,否则内部的数据即会消失,因此SRAM具有较高的性能,但是SRAM也有它的缺点,即它的集成度较低,相同容量的DRAM内存可以设计为较小的体积,但是SRAM却需要很大的体积,这也是不能将缓存容量做得太大的重要原因。它的特点归纳如下:优点是节能、速率快、不必配合内存刷新电路、可提高整体的工作效率,缺点是集成度低、相同的容量体积较大、而且价格较高,只能少量用于关键性系统以提高效率。 @@ -272,7 +274,7 @@ CPU 是计算机的大脑,它主要和内存进行交互,从内存中提取 - +- [下一篇:2.进程与线程][https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/2.%E8%BF%9B%E7%A8%8B%E4%B8%8E%E7%BA%BF%E7%A8%8B.md] diff --git "a/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" index 5f19d32d..92e17b16 100644 --- "a/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" +++ "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" @@ -294,7 +294,9 @@ +- [上一篇:1.操作系统简介][https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/1.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%AE%80%E4%BB%8B.md] +- [下一篇:2.进程与线程][https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/2.%E8%BF%9B%E7%A8%8B%E4%B8%8E%E7%BA%BF%E7%A8%8B.md] --- From db0f8bb241c36c4701b22274b39d6572986954e7 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 26 Nov 2020 13:28:20 +0800 Subject: [PATCH 035/213] update operation syste --- ...73\347\273\237\347\256\200\344\273\213.md" | 2 +- ...13\344\270\216\347\272\277\347\250\213.md" | 137 +++++++++++------- ...05\345\255\230\347\256\241\347\220\206.md" | 62 ++++---- .../4.\350\260\203\345\272\246.md" | 5 +- OperatingSystem/5.I:O.md | 3 + ...07\344\273\266\347\256\241\347\220\206.md" | 5 + ...45\345\274\217\347\263\273\347\273\237.md" | 3 + ...8.\350\231\232\346\213\237\346\234\272.md" | 75 +--------- 8 files changed, 137 insertions(+), 155 deletions(-) diff --git "a/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" index b6dd93ea..1129070f 100644 --- "a/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" +++ "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" @@ -274,7 +274,7 @@ CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从 -- [下一篇:2.进程与线程][https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/2.%E8%BF%9B%E7%A8%8B%E4%B8%8E%E7%BA%BF%E7%A8%8B.md] +- [下一篇:2.进程与线程](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/2.%E8%BF%9B%E7%A8%8B%E4%B8%8E%E7%BA%BF%E7%A8%8B.md) diff --git "a/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" index 92e17b16..316d0c47 100644 --- "a/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" +++ "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" @@ -10,7 +10,7 @@ -进程是60年代初首先由[麻省理工学院](https://baike.baidu.com/item/麻省理工学院)的[MULTICS系统](https://baike.baidu.com/item/MULTICS系统)和IBM公司的[CTSS](https://baike.baidu.com/item/CTSS)/360系统引入的。 [2] +进程是60年代初首先由[麻省理工学院](https://baike.baidu.com/item/麻省理工学院)的[MULTICS系统](https://baike.baidu.com/item/MULTICS系统)和IBM公司的[CTSS](https://baike.baidu.com/item/CTSS)/360系统引入的。 进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。它可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体。它不只是程序的[代码](https://baike.baidu.com/item/代码),还包括当前的活动,通过[程序计数器](https://baike.baidu.com/item/程序计数器)的值和处理[寄存器](https://baike.baidu.com/item/寄存器)的内容来表示。 @@ -58,8 +58,6 @@ -进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对 PCB 的操作。 - ## 进程的状态 @@ -76,34 +74,22 @@ - 程序段:对应程序的操作代码部分,用于描述进程所需要完成的功能。 - 数据段:对应程序执行时所需要的数据部分,包括数据,堆栈和工作区。 -- 进程控制块:记录了进程运行时所需要的全部信息,它是进程存在的唯一标识,与进程一一对应。 +- 进程控制块(Process Control Block, PCB) :描述进程的基本信息和运行状态,记录了进程运行时所需要的全部信息,它是进程存在的唯一标识,与进程一一对应。所谓的创建进程和撤销进程,都是指对 PCB 的操作。 -##### 进程控制块 +#### 进程控制块 进程执行的任意时刻,都可由如下元素来表征: -- 标识符:与进程相关的唯一标识符,用来区分其他进程 -- 状态:若进程正在执行,则进程处于运行态 -- 优先级:相对于其他进程的优先顺序 +- 标识符:与进程相关的唯一标识符,用于标识、区分一个进程,通常有外部标识符和内部标识符两类。外部标识符通常是由字母、数字所组成的一个字符串,用户或其他进程访问该进程时使用。内部标识符是操作系统为每个进程赋予的唯一一个整数,是作为内部识别而设置的。 +- 状态:若进程正在执行,则进程处于运行态。进程状态指明进程当前的状态,作为进程调度和对换时的依据; +- 优先级:相对于其他进程的优先顺序,说明进程使用CPU的优先级别,其中优先级高的进程将优先获得CPU。 - 程序计数器:程序中即将执行的下一条指令的地址 - 内存指针:包括程序代码和进程相关数据的指针,以及与其他进程共享内存块的指针 - 上下文数据:进程执行时处理器的寄存器中的数据 - I/O状态信息:包括显式I/O请求、分配给进程的I/O设备和被进程使用的文件列表等 - 记账信息:包括处理器时间总和、使用的时钟数总和、时间限制、及账号等 -上述列表信息存放在一个被称为进程控制块(process control block)的数据结构中,控制块由操作系统创建和管理。进程控制块(PCB)是进程实体的重要组成部分,它记录了操作系统所需要的、用于描述进程情况及控制进程所需要的全部信息。原来不能独立运行的程序或数据,通过 PCB 就可以成为一个可以独立运行的基本单位。系统通过 PCB 感知进程的存在,并对其进行有效管理和控制。系统创建一个新进程时,为它建立一个 PCB;当进程结束时,系统又收回其 PCB,该进程也随之消亡。简单的总结就是进程控制块主要包括下述四个方面的信息: - -1. 进程标识符信息:用于标识、区分一个进程,通常有外部标识符和内部标识符两类。外部标识符通常是由字母、数字所组成的一个字符串,用户或其他进程访问该进程时使用。内部标识符是操作系统为每个进程赋予的唯一一个整数,是作为内部识别而设置的。 - -2. 进程调度信息:用于描述与进程调度有关的状态信息,包括进程状态、进程优先权、调度信息和等待事件等。进程状态指明进程当前的状态,作为进程调度和对换时的依据;进程优先权说明进程使用 CPU 的优先级别,其中优先权高的进程将优先获得 CPU;调度信息描述与进程调度算法相关的信息,如进程等待时间、已运行的时间等;等待事件是指进程由运行态转变为阻塞态时所等待发生的事件。 - -3. CPU 状态信息:用于保留进程运行时 CPU 的各种信息,使得进程暂停运行后,下次重新运行时能从上次停止的地方继续运行。CPU 状态信息通常包含通用寄存器、控制和状态寄存器、用户栈指针等。CPU 状态字记载了程序执行的状态信息,如条件码、外中断屏蔽标识、执行状态(核心态或用户态)标识等。 - -4. 进程控制信息(process control information):包括进程资源、控制机制等一些进程运行时所需要的信息,如: - - 程序和数据地址:该进程的程序和数据所在的内存和外存地址,以便该进程在次运行时,能够找到程序和数据。 - - 进程同步和通信机制:实现进程同步和通信时所采用的机制,如消息队列指针、信号量等。 - - 资源清单:除 CPU 外,进程所需的全部资源和已经分配到的资源。 - - 链接指针:用于指向该进程所在队列的下一个进程的 PCB 首地址。 +上述列表信息存放在一个被称为进程控制块(process control block)的数据结构中,控制块由操作系统创建和管理。系统通过 PCB 感知进程的存在,并对其进行有效管理和控制。系统创建一个新进程时,为它建立一个PCB;当进程结束时,系统又收回其PCB,该进程也随之消亡。 @@ -121,7 +107,9 @@ ## 进程切换 +操作系统为了执行进程间的切换,会维护着一张表格,这张表就是 进程表(process table)。每个进程占用一个进程表项。该表项包含了进程状态的重要信息,包括程序计数器、堆栈指针、内存分配状况、所打开文件的状态、账号和调度信息,以及其他在进程由运行态转换到就绪态或阻塞态时所必须保存的信息,从而保证该进程随后能再次启动,就像从未被中断过一样。 +**操作系统最底层的就是调度程序**,在它上面有许多进程。所有关于中断处理、启动进程和停止进程的具体细节都隐藏在调度程序中。事实上,调度程序只是一段非常小的程序。 表面上看,进程切换很简单。在某个时刻,操作系统中断一个正在运行的进程,将另一个进程置于运行模式,并把控制权交给后者。然而,这会引发若干个问题。首先,什么事件触发了进程的切换? 其次,必须认识到模式切换和进程切换键的区别。 @@ -159,7 +147,7 @@ ### 互斥的要求 1. 必须强制实施互斥。在于相同资源或共享对象的临界区有关的所有进程中,一次只允许一个进程进入临界区。 -2. 一个在非临界区停止的进程不能干啥其他进程。 +2. 一个在非临界区停止的进程不能干涉其他进程。 3. 绝不允许出现需要访问临界区的进程被无限延迟的情况,即不会死锁或饥饿。 4. 没有进程在临界区中时,任何需要进入临界区的进程必须能够立即进入。 5. 对相关进程的执行速度和处理器的数量没有任何要求和限制。 @@ -193,24 +181,6 @@ -**操作系统最底层的就是调度程序**,在它上面有许多进程。所有关于中断处理、启动进程和停止进程的具体细节都隐藏在调度程序中。事实上,调度程序只是一段非常小的程序。 - - - -### 进程的实现 - -操作系统为了执行进程间的切换,会维护着一张表格,这张表就是 `进程表(process table)`。每个进程占用一个进程表项。该表项包含了进程状态的重要信息,包括程序计数器、堆栈指针、内存分配状况、所打开文件的状态、账号和调度信息,以及其他在进程由运行态转换到就绪态或阻塞态时所必须保存的信息,从而保证该进程随后能再次启动,就像从未被中断过一样。 - - - -**操作系统最底层的就是调度程序**,在它上面有许多进程。所有关于中断处理、启动进程和停止进程的具体细节都隐藏在调度程序中。事实上,调度程序只是一段非常小的程序。 - - - -操作系统为了执行进程间的切换,会维护着一张表格,这张表就是 `进程表(process table)`。每个进程占用一个进程表项。该表项包含了进程状态的重要信息,包括程序计数器、堆栈指针、内存分配状况、所打开文件的状态、账号和调度信息,以及其他在进程由运行态转换到就绪态或阻塞态时所必须保存的信息,从而保证该进程随后能再次启动,就像从未被中断过一样。 - - - ## 进程调度 进程调度就是处理器调度(上下文切换) @@ -219,15 +189,15 @@ - 高级调度 -作业调度,把后备作业调入内存运行 + 作业调度,把后备作业调入内存运行 - 中级调度 -在虚拟存储器中引入,在内,外存交换区进行进程对换 + 在虚拟存储器中引入,在内,外存交换区进行进程对换 - 低级调度 -进程调度,把就绪队列里的某个进程获得CPU执行权 + 进程调度,把就绪队列里的某个进程获得CPU执行权 ### 调度方式 @@ -237,18 +207,18 @@ - 不可剥夺 -一单处理器分配给某进程,遍让它一直运行下去,直到进程完成或者发生某种时间而阻塞,才分配给其他进程。 + 一单处理器分配给某进程,遍让它一直运行下去,直到进程完成或者发生某种时间而阻塞,才分配给其他进程。 ### 调度算法 - 先进先出 -按照进入就绪队列的进程顺序,不加其他条件干涉 + 按照进入就绪队列的进程顺序,不加其他条件干涉 - 短进程优先 -优先选出就绪队列中CPU执行时间最短的进程,例如:就绪队列有4个进程P1,P2,P3,P4,执行时间为:16,12,4,3 按照短进程优先,则周转时间(从进程提交到进程完成的时间间隔)分别为:35,19,7,3 -平均周转时间:16,平均周转时间越小,调度性能越好 + 优先选出就绪队列中CPU执行时间最短的进程,例如:就绪队列有4个进程P1,P2,P3,P4,执行时间为:16,12,4,3 按照短进程优先,则周转时间(从进程提交到进程完成的时间间隔)分别为:35,19,7,3 + 平均周转时间:16,平均周转时间越小,调度性能越好 - 轮转法 - 简单轮转: 就绪进程按FIFO排队,按照一定时间间隔让处理机分配给队列中的进程,就绪队列中**所有队列均可获得一个时间片的处理器运行** @@ -274,7 +244,76 @@ +## Android进程结构 + +如同传统的Linux系统一样,Android的第一个用户空间进程是init,它是所有其他进程的根。然而,Android的init启动的守护进程是不同的,这些守护进程更多的聚焦于底层细节(管理文件系统和硬件访问),而不是高层用户设施,例如调度定时任务。Android还有一层额外的进程,它们运行Dalvik的Java语言环境,负责执行系统中所有以Java实现的部分。 + + + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_process.png?raw=true) + + + +如上图,首先是init进程,它产生了一些底层守护进程。其中一个守护进程是zygote,它是高级Java语言进程的根。Android的init不以传统的方法运行shell,因为典型的Android设备没有本地控制台用于shell访问。作为替代,系统进程adbd监听请求shell访问的远程连接(例如通过USB),按要求为它们创建shell进程。因为Android大部分是用Java语言编写的,所以zygote守护进程以及由它启动的进程是系统的中心。由zygote启动的第一个进程称为system_server,它包含全部核心操作服务,其关键部分是电源管理、包管理、窗口管理和活动管理。 + +其他进程在需要的时候由zygote创建。这些进程中有一些是“持久的”进程,它们是基本操作系统的组成部分,例如phone进程中的电话栈,它必须保持始终运行。另外的应用程序进程将在系统运行的过程中按需创建和终止。 + +应用程序通过调用操作系统提供的库与操作系统进行交互,这些库合起来构成Android框架(Android framework)。这些库中有一些可以在进程内部执行其工作,但是许多库需要与其他进程执行进程间通信,作者通常是在system_server进程中提供服务的。 + + + +### Zygote + +Zygote是在启动时就运行在DVM上的一个进程。每当出现创建进程的请求时,Zygote就会产生一个新的DVM虚拟机。Zygote通过在内存中尽可能多的共享内容来最小化产生一个新DVM所消耗的时间。通常而言,许多应用程序都会使用核心库的类和相应的堆结构,而这些内容都是只读的。也就是说,被大部分应用程序使用的这些共享数据和类都是只读而不能改变的。因此,当Zygote加载时,就预加载和初始化了应用程序运行时可能用到的Java核心库和资源。当Zygote创建一个新的DVM时,这部分类不会被分配到新内存。Zygote只是简单地把子进程的这些内存页映射到父进程的相应位置。 + +实际上,几乎不需要更多的映射页。如果一个类被一个子进程自己的DVM改写,那么Zygote会将受影响的内存复制到子进程中。这种即写即复制的行为在使得最大化共享内存的同时,还能保证应用程序间不会相互影响,并在跨应用程序和进程的边界时保证安全性。 + + + +## Android进程模型 + + + +Linux的传统进程模型是用fork指令来创建新进程,然后用exec指令使用待运行的源码初始化该进程并开始执行。shell负责实现进程执行、创建新进程、执行所需的进程来运行shell指令。当指令结束时,进程被从Linux中移除。 + +Android使用的进程有些不同。活动管理器是Android负责正在运行的应用程序的管理的一部分。活动管理器协调新应用程序进程的启动,决定哪些应用程序能在其中运行,哪些已不再需要。 + + + +### 启动进程 + +为了启动新进程,活动管理器需要与zygote通信。活动管理器首先开始,它创建一个与zygote相连的专用接口,通过接口发送一条指令,表示它需要启动一个进程。这条指令主要描述需要创建的沙箱、新进程运行所需要的UID以及需要遵守的安全性制约。zygote需要作为根来运行:创建新进程时,它合理配置运行所需的UID,最终下放权限,将进程改为该UID。 + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_start_process.png?raw=true) + +上图展示了一个新进程中启动活动的流程: + +1. 某个现有进程(如应用程序启动器)调用活动管理器,发出意图,描述它想要启动的新活动。 +2. 活动管理器要求封装管理器将这个意图解析为一个明确的组件。 +3. 活动管理器判断这个应用程序的进程并未正在运行,然后向zygote请求一个具有合适UID的新锦成。 +4. zygote进行一次fork指令,克隆自己来创造一个新进程,下方权限并配置新进程的UID和沙箱,初始化该进程的Dalvik,使得Java runtime开始完全执行。例如,它需要在fork后启动垃圾收集等线程。 +5. 新进程如今是一个zygote的克隆,并运行着完全配置好的Java环境。它回调活动管理器,询问后者“我该做什么”。 +6. 活动管理器返回即将启动的应用程序的完整信息,如源码位置等。 +7. 新进程读取应用程序的源码,开始运行。 +8. 活动管理器将所有即将进行的操作发送给新进程,在此处为“启动活动X”。 +9. 新进程收到指令,启动活动,实体化合适的Java类并执行。 + +注意,当活动启动时,应用程序的进程可能正在运行了。在这种情况下,活动管理器会直接跳转到末尾,向该进程发送一条新指令,让它实体化并执行合适的组件。如果合适,这会导致一个额外的活动实例在应用程序中运行。 + + + +### 进程生命周期 + +活动管理器也负责判断何时进程不再被需要。活动管理器记录一个进程中运行的所有活动、接收器、服务以及内容提供其,据此可判断该进程的重要程度。 + +Android内核中的内存溢出强制结束指令是使用一个进程的oom_adj进行严格排序,决定哪个进程需要优先强制结束。活动管理器负责基于每个进程的状态,通过将其归类于几个主要用途,从而合理设定器oom_adj。/proc//oom_adj: + +- 取值是-17到+15,取值越高,越容易被干掉。如果是-17,则表示不能被kill +- 该设置参数的存在是为了和旧版本的内核兼容 + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_process_adj.png?raw=true) +让RAM内存不足时,系统已经完成了进程的配置,使得内存溢出强制结束命令优先中止缓存(cache)类型的进程,尝试重新取得足够的所需内存,随后中止界面(home)类别、服务(service)类别,以此类推。在同一个oom_adj水平中,它将优先中止内存占用较大的进程。 @@ -294,9 +333,9 @@ -- [上一篇:1.操作系统简介][https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/1.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%AE%80%E4%BB%8B.md] +- [上一篇:1.操作系统简介](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/1.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%AE%80%E4%BB%8B.md) -- [下一篇:2.进程与线程][https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/2.%E8%BF%9B%E7%A8%8B%E4%B8%8E%E7%BA%BF%E7%A8%8B.md] +- [下一篇:2.进程与线程](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/2.%E8%BF%9B%E7%A8%8B%E4%B8%8E%E7%BA%BF%E7%A8%8B.md) --- diff --git "a/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" "b/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" index a4b40e39..3a54aa0e 100644 --- "a/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" +++ "b/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" @@ -2,28 +2,31 @@ 常用概念: -- 页框:内存中固定长度的快 -- 页:固定长度的数据库,存储在二级存储器中(如磁盘)。数据页可以临时赋值到内存的页框中。 +- 页框:内存中固定长度的块 +- 页:固定长度的数据块,存储在二级存储器中(如磁盘)。数据页可以临时赋值到内存的页框中。 - 段:变长数据块,存储在二级存储器中。整个段可以临时复制到内存的一个可用区域中(分段),或可以将一个段分为许多页,然后将每页单独复制到内存中(分段与分页相结合) -内存管理的主要操作是处理器把程序装入内存中执行。内存管理的功能有: -1、内存空间的分配与回收:由操作系统完成主存储器空间的分配和管理,使程序员摆脱存储分配的麻烦,提高编程效率。 -2、地址转换:在多道程序环境下,程序中的逻辑地址与内存中的物理地址不可能一致,因此存储管理必须提供地址变换功能,把逻辑地址转换成相应的物理地址。 -3、内存空间的扩充:利用虚拟存储技术或自动覆盖技术,从逻辑上扩充内存。 -4、存储保护:保证各道作业在各自的存储空间内运行,互不干扰。 +## 内存管理的功能 + +内存管理的主要操作是处理器把程序装入内存中执行。内存管理的功能有: + +1. 内存空间的分配与回收:由操作系统完成主存储器空间的分配和管理,使程序员摆脱存储分配的麻烦,提高编程效率。 +2. 地址转换:在多道程序环境下,程序中的逻辑地址与内存中的物理地址不可能一致,因此存储管理必须提供地址变换功能,把逻辑地址转换成相应的物理地址。 +3. 内存空间的扩充:利用虚拟存储技术或自动覆盖技术,从逻辑上扩充内存。 +4. 存储保护:保证各道作业在各自的存储空间内运行,互不干扰。 进程对应的内存空间中所包含的5种不同的数据区: -- 代码段(code segment):又称文本段,用来存放指令,运行代码的一块内存空间。此空间大小在代码运行前就已经确定。内存空间一般属于只读,某些架构的代码也允许可写。 +- 代码段(code segment):用来存放程序运行代码的一块内存空间。此空间大小在代码运行前就已经确定。内存空间一般属于只读,某些架构的代码也允许可写。 - 数据段(data segment): 存储初始化的全局变量和初始化的static变量。数据段中的数据的生存期是随程序持续性(随进程持续性):进程创建就存在,进程死亡就消失。 -- BSS段(bss segment):存储未初始化的全局变量和未初始化的static变量。bss段中数据的生存期随进程持续性。bss段中的数据一般默认为0. -- rodata段:只读数据 比如 printf 语句中的格式字符串和开关语句的跳转表。也就是常量区。例如,全局作用域中的 const int ival = 10,ival 存放在 .rodata 段;再如,函数局部作用域中的 printf("Hello world %d\n", c); 语句中的格式字符串 "Hello world %d\n",也存放在 .rodata 段。 +- BSS段(bss segment):存储未初始化的全局变量和未初始化的static变量。bss段中数据的生存期随进程持续性。bss段中的数据一般默认为0.这里很奇怪,一般的书上都会说全局变量和静态变量是会自动初始化的,怎么突然来了个未初始化的变量?其实变量的初始化可以分为显式初始化和隐式初始化,全局变量和静态变量如果程序员自己不初始化的话也会被初始化,那就是不管什么类型都初始化为默认值,这种没有显示初始化的就是这里所说的未初始化。例如整数型的全局变量未初始化的默认隐式初始化的值为0,都是0就没必要把每个0都存储起来,从而节省磁盘空间,这是BSS的主要作用)。BSS的全称是Block Started by Symbol,它属于静态内存分配。BSS节不包含任何数据,只是简单的维护开始和结束的地址,即总大小,以便内存能在运行时分配并被有效的清零。BSS节在应用程序的二进制映像文件中并不存在,既不占用磁盘空间,而只在运行的时候占用内存空间,所以如果全局变量和静态变量未初始化那么其可执行文件要小很多。 +- rodata段(read only data):常量区,存放只读数据。比如程序中定义为const的全局变量,“Hello Word”的字符串常量。有些系统中rodata段是多个进程共享的,目的是为了提高空间利用率。在有的嵌入式系统中,rodata放在ROM中,运行时直接读取,不须加载到RAM内存中。所以在嵌入式开发中,常将已知的常量系数,表格数据等加以const关键字。存放在ROM中,避免占用RAM空间。 - 堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc、realloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减) @@ -39,19 +42,17 @@ ![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/memory_1.jpg?raw=true) -Stack 和 Heap 中间有一块 free space,即使没有用,也被占着,那如何才能解放这块区域呢,进入虚拟内存。 - +Stack 和 Heap 中间有一块 free space,即使没有用,也被占着,那如何才能解放这块区域呢,那就是虚拟内存。 - -Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自互不干涉的进程地址空间。该空间是块大小为4G的线性虚拟空间,用户所看到和接触到的都是该虚拟地址,无法看到实际的物理内存地址。利用这种虚拟地址不但能起到保护操作系统的效果(用户不能直接访问物理内存),而且更重要的是,用户程序可使用比实际物理内存更大的地址空间(具体的原因请看硬件基础部分)。 +Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自互不干涉的进程地址空间。该空间是块大小为4G的线性虚拟空间,用户所看到和接触到的都是该虚拟地址,无法看到实际的物理内存地址。利用这种虚拟地址不但能起到保护操作系统的效果(用户不能直接访问物理内存),而且更重要的是,用户程序可使用比实际物理内存更大的地址空间。 在讨论进程空间细节前,这里先要澄清下面几个问题: -l 第一、4G的进程地址空间被人为的分为两个部分——用户空间与内核空间。用户空间从0到3G(0xC0000000),内核空间占据3G到4G。用户进程通常情况下只能访问用户空间的虚拟地址,不能访问内核空间虚拟地址。只有用户进程进行系统调用(代表用户进程在内核态执行)等时刻可以访问到内核空间。 +1. 4G的进程地址空间被人为的分为两个部分——用户空间与内核空间。用户空间从0到3G(0xC0000000),内核空间占据3G到4G。用户进程通常情况下只能访问用户空间的虚拟地址,不能访问内核空间虚拟地址。只有用户进程进行系统调用(代表用户进程在内核态执行)等时刻可以访问到内核空间。 -l 第二、用户空间对应进程,所以每当进程切换,用户空间就会跟着变化;而内核空间是由内核负责映射,它并不会跟着进程改变,是固定的。内核空间地址有自己对应的页表(init_mm.pgd),用户进程各自有不同的页表。 +2. 用户空间对应进程,所以每当进程切换,用户空间就会跟着变化;而内核空间是由内核负责映射,它并不会跟着进程改变,是固定的。内核空间地址有自己对应的页表(init_mm.pgd),用户进程各自有不同的页表。 -l 第三、每个进程的用户空间都是完全独立、互不相干的。不信的话,你可以把上面的程序同时运行10次(当然为了同时运行,让它们在返回前一同睡眠100秒吧),你会看到10个进程占用的线性地址一模一样。 +3. 每个进程的用户空间都是完全独立、互不相干的。不信的话,你可以把上面的程序同时运行10次(当然为了同时运行,让它们在返回前一同睡眠100秒吧),你会看到10个进程占用的线性地址一模一样。 @@ -71,7 +72,7 @@ l 第三、每个进程的用户空间都是完全独立、互不相干的 - 程序可能太大而不能放到一个分区中。此时程序员必须使用覆盖技术设计程序,使得任何时候该程序只有一部分需要放到内存中。当需要的模块不在时,用户程序必须把这个模块装入程序的分区,覆盖该分区中的任何程序和数据。 - - 内存的利用率非常低。任何程序,急事很小,都需要占据一个完整的分区。由于装入的数据块小于分区大小,因而导致分区内部存在空间浪费,这种现象称为内部碎片(internal fragmentation)。 + - 内存的利用率非常低。任何程序,即使很小,都需要占据一个完整的分区。由于装入的数据块小于分区大小,因而导致分区内部存在空间浪费,这种现象称为内部碎片(internal fragmentation)。 - 使用大小不等的分区:大小不等的分区可以缓解上面的两个问题,但是不能完全解决。 @@ -83,7 +84,7 @@ l 第三、每个进程的用户空间都是完全独立、互不相干的 为了克服固定分区的确定,提出了动态分区。对于动态分区,分区长度和数量是可变的。程序装入内存时,系统会给它分配一块与其所需容量完全相等的内存空间。但是对于一个64MB的内存,如果装入前三个进程已经占用了很大一部分,剩下的部分对于第四个进程来说又太小,这样在内存的末尾就剩下一个“空洞”。而等第二个进程结束后腾出的足够的空间来装入第四个进程,但是由于第四个进程比第二个进程小,所有这里又形成了另一个小"空洞"。这样最终在内存中就会形成许多小空洞。随着时间的推移,内存中形成了越来越多的碎片,内存的利用率随之下降。这种现象称为外部碎片(external fragmentation),指在所有分区外的存储空间变成了越来越多的碎片,这与前面所讲的内部碎片正好对应。 -客服外部碎片的一种技术就是压缩(compaction)。操作系统不时的移动进程,使得进程占用的空间连续,并使所有空闲空间连成一片。但是压缩的困难之处在于,它是一个非常费时的过程,且会浪费处理器时间。 +克服外部碎片的一种技术就是压缩(compaction)。操作系统不时的移动进程,使得进程占用的空间连续,并使所有空闲空间连成一片。但是压缩的困难之处在于,它是一个非常费时的过程,且会浪费处理器时间。 @@ -97,9 +98,9 @@ l 第三、每个进程的用户空间都是完全独立、互不相干的 -在大小相等的分区中一个进程在其声明周期中可能占据不同的分区。首次创建一个进程映像时,它被装入内存中的摸个分区。以后该进程可能被换出,当它再次被换入时,可能被指定到与上一次不同的分区中。动态分区也存在同样的情况,压缩后内存中的进程也可能发生移动。因此,进程访问(指令和数据单元)的位置不是固定的。进程被换入或在内存中移动时,指令和数据单元的位置也发生变化。为了解决这个问题,需要区分几种地址类型: +在大小相等的分区中一个进程在其声明周期中可能占据不同的分区。首次创建一个进程映像时,它被装入内存中的某个分区。以后该进程可能被换出,当它再次被换入时,可能被指定到与上一次不同的分区中。动态分区也存在同样的情况,压缩后内存中的进程也可能发生移动。因此,进程访问(指令和数据单元)的位置不是固定的。进程被换入或在内存中移动时,指令和数据单元的位置也发生变化。为了解决这个问题,需要区分几种地址类型: -- 逻辑地址(logical address)是指与当前数据再内存中的物理分配地址无关的访问地址,在执行对内存的访问之前必须把它转换为物理地址。 +- 逻辑地址(logical address)是指与当前数据在内存中的物理分配地址无关的访问地址,在执行对内存的访问之前必须把它转换为物理地址。 - 相对地址(relative address)是逻辑地址的一个特例,他是相对于某些已知点(通常是程序的开始处)的存储单元。 - 物理地址(physical address)或绝对地址是数据在内存中的实际位置。 @@ -125,7 +126,7 @@ l 第三、每个进程的用户空间都是完全独立、互不相干的 ### 分段 -细分用户程序的另一种可选方案是分段。采用分段技术,可以把程序和与其相关的数据划分到几个段(fragment)中。尽管短有最大长度限制,但并不要求所有程序的所有段的长度都相等。和分页一样,采用分段技术时的逻辑地址也是由两部分组成:段号和偏移量。 +细分用户程序的另一种可选方案是分段。采用分段技术,可以把程序和与其相关的数据划分到几个段(fragment)中。尽管段有最大长度限制,但并不要求所有程序的所有段的长度都相等。和分页一样,采用分段技术时的逻辑地址也是由两部分组成:段号和偏移量。 由于使用大小不等的段,分段类似于动态分区。在未采用覆盖方案或使用虚存的情况下,为执行一个程序,需要把它的所有段都装入内存。与动态分区不同的是,在分段方案中,一个程序可以占据多个分区,并且这些分区不要求是连续的。分段消除了内部碎片,但是和动态分区一样,他会产生外部碎片。不过由于进程被分成多个小块,因此外部碎片也会很小。 @@ -163,7 +164,7 @@ l 第三、每个进程的用户空间都是完全独立、互不相干的 虚拟内存机制使得期望运行大于物理内存的程序称为可能。其方法是将程序放在磁盘上,而将主存作为一种缓存,用来保存最频繁使用的部分程序。这种机制需要快速的映像内存地址,以便把程序生成的地址转换为有关字节在RAM中的物理地址。这种映像由CPU中的一个称为存储器管理单元(Memory Management Unit,MMU)的部件来完成。 -**虚拟内存**的**基本思想**是:每个程序都拥有自己的地址空间,这个空间被分割成多个 块,每个块被成为一页或页面. +**虚拟内存**的**基本思想**是:每个程序都拥有自己的地址空间,这个空间被分割成多个块,每个块被成为一页或页面. **程序运行时,并不是所有页都在物理内存中**: @@ -204,7 +205,7 @@ l 第三、每个进程的用户空间都是完全独立、互不相干的 #### 2. 放置策略 -防止策略决定一个进程块驻留在实存中的什么位置。在纯分页系统或段页式系统中,如何放置通常无关紧要,因为地址转换硬件和内存访问硬件能以相同的效率为任何页框组合执行相应的功能。 +放置策略决定一个进程块驻留在实存中的什么位置。在纯分页系统或段页式系统中,如何放置通常无关紧要,因为地址转换硬件和内存访问硬件能以相同的效率为任何页框组合执行相应的功能。 #### 3. 置换策略 @@ -218,15 +219,15 @@ l 第三、每个进程的用户空间都是完全独立、互不相干的 -#### 5.清楚策略 +#### 5.清除策略 -与读取策略相反,清楚策略用于确定何时将已修改的一页写回辅存。通常有两种选择: +与读取策略相反,清除策略用于确定何时将已修改的一页写回辅存。通常有两种选择: -- 请求式清楚 +- 请求式清除 只有当一页被选择用于置换时才能被写回辅存。 -- 预约式清楚 +- 预约式清除 将这些已修改的多页在需要使用他们所占据的页框之前成批写回辅存。 @@ -244,8 +245,7 @@ Android包含了标准Linux内核中内存管理设施的许多扩展,具体 - ASHMem:这个功能提供匿名共享内存,它将内存抽象为文件描述符。文件描述符可以传递给另一个进程以共享内存。 - Pmen:这个功能分配虚拟内存,使的它在物理上是连续的,因此对于那些不支持虚拟内存的设备非常实用。 -- Low Memory Killer:大部分移动设备不具备置换能力(因为闪存的使用寿命因素)。主存耗尽时,使用大量内存的应用不是让步对内存的使用就是被终结。这个功能可让系统通知应用释放内存,如果应用不配合,则终结应用。 - +- Low Memory Killer:ndorid基于oomKiller原理所扩展的一个多层次oomKiller,OOMkiller(Out Of Memory Killer)是在Linux系统无法分配新内存的时候,选择性杀掉进程,到oom的时候,系统可能已经不太稳定,而LowMemoryKiller是一种根据内存阈值级别触发的内存回收的机制,在系统可用内存较低时,就会选择性杀死进程的策略,相对OOMKiller,更加灵活。主存耗尽时,使用大量内存的应用不是让步对内存的使用就是被终结。这个功能可让系统通知应用释放内存,如果应用不配合,则终结应用。 @@ -263,6 +263,8 @@ Android包含了标准Linux内核中内存管理设施的许多扩展,具体 +- [上一篇:2.进程和线程](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/2.%E8%BF%9B%E7%A8%8B%E4%B8%8E%E7%BA%BF%E7%A8%8B.md) +- [下一篇:4.调度](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/4.%E8%B0%83%E5%BA%A6.md) diff --git "a/OperatingSystem/4.\350\260\203\345\272\246.md" "b/OperatingSystem/4.\350\260\203\345\272\246.md" index bb435432..568d6cf7 100644 --- "a/OperatingSystem/4.\350\260\203\345\272\246.md" +++ "b/OperatingSystem/4.\350\260\203\345\272\246.md" @@ -51,7 +51,7 @@ - 把进程分配到处理器 - 假设多处理器的结构是统一的,即没有哪个处理器在访问内核和I/O设备时具有物理上的特别优势,那么最简单的调度方法是把处理器视为一个资源池,并按照要求把进程分配到相应的处理器。但是这样就牵扯到静态还是动态的问题。如果一个进程从被激活到完成,一直被分配给同一个处理器,那么就需要为每个处理器维护一个专门的短程队列。这种方法的优点是调度的开销较小,因为相对于所有进程,关于处理器的分配只进行一次。静态分配的缺点就是一个处理器可能处于空闲状态,这时其队列为空,而另一个处理器却积压了许多工作。为了防止这种情况,需要使用一个公共队列。所有进程都进入一个全局队列,然后调度到任何一个可用的处理器中。这样,在一个进程的声明周期中,它可以在不同的时间于不同的处理器上执行。另一种分配策略是动态负载平衡,在该策略中,线程能在不同处理器所对应的队列之间转移。Linux采用的就是这种动态分配策略。 + 假设多处理器的结构是统一的,即没有哪个处理器在访问内核和I/O设备时具有物理上的特别优势,那么最简单的调度方法是把处理器视为一个资源池,并按照要求把进程分配到相应的处理器。但是这样就牵扯到静态还是动态的问题。如果一个进程从被激活到完成,一直被分配给同一个处理器,那么就需要为每个处理器维护一个专门的短程队列。这种方法的优点是调度的开销较小,因为相对于所有进程,关于处理器的分配只进行一次。静态分配的缺点就是一个处理器可能处于空闲状态,这时其队列为空,而另一个处理器却积压了许多工作。为了防止这种情况,需要使用一个公共队列。所有进程都进入一个全局队列,然后调度到任何一个可用的处理器中。这样,在一个进程的生命周期中,它可以在不同的时间于不同的处理器上执行。另一种分配策略是动态负载平衡,在该策略中,线程能在不同处理器所对应的队列之间转移。Linux采用的就是这种动态分配策略。 @@ -80,6 +80,9 @@ +- [上一篇:3.内存管理](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/3.%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86.md) +- [下一篇:5.I/O](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/5.I:O.md) + --- diff --git a/OperatingSystem/5.I:O.md b/OperatingSystem/5.I:O.md index bf043437..2a4e19c2 100644 --- a/OperatingSystem/5.I:O.md +++ b/OperatingSystem/5.I:O.md @@ -33,6 +33,9 @@ +- [上一篇:4.调度](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/4.%E8%B0%83%E5%BA%A6.md) +- [下一篇:6.文件管理](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/6.%E6%96%87%E4%BB%B6%E7%AE%A1%E7%90%86.md) + diff --git "a/OperatingSystem/6.\346\226\207\344\273\266\347\256\241\347\220\206.md" "b/OperatingSystem/6.\346\226\207\344\273\266\347\256\241\347\220\206.md" index 50a05239..3621dd4d 100644 --- "a/OperatingSystem/6.\346\226\207\344\273\266\347\256\241\347\220\206.md" +++ "b/OperatingSystem/6.\346\226\207\344\273\266\347\256\241\347\220\206.md" @@ -1,5 +1,7 @@ # 6.文件管理 + + 文件管理系统是一组系统软件,它为使用文件的用户和应用程序提供服务,包括文件访问、目录维护和访问控制。文件管理系统通常被视为一个由操作系统提供服务的系统服务,而不是操作系统的一部分,但是在任何系统中,至少有一部分文件管理功能是由操作系统执行的。 @@ -54,6 +56,9 @@ Android文件系统目录的顶层部分: +- [上一篇:5.I/O](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/5.I:O.md) +- [下一篇:7.嵌入式系统](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/7.%E5%B5%8C%E5%85%A5%E5%BC%8F%E7%B3%BB%E7%BB%9F.md) + diff --git "a/OperatingSystem/7.\345\265\214\345\205\245\345\274\217\347\263\273\347\273\237.md" "b/OperatingSystem/7.\345\265\214\345\205\245\345\274\217\347\263\273\347\273\237.md" index 5cddf7fc..e37630e3 100644 --- "a/OperatingSystem/7.\345\265\214\345\205\245\345\274\217\347\263\273\347\273\237.md" +++ "b/OperatingSystem/7.\345\265\214\345\205\245\345\274\217\347\263\273\347\273\237.md" @@ -20,6 +20,9 @@ Android是基于Linux内核的一个嵌入式系统,因此我们可以认为An +- [上一篇:6.文件管理](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/6.%E6%96%87%E4%BB%B6%E7%AE%A1%E7%90%86.md) +- [下一篇:8.虚拟机](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/8.%E8%99%9A%E6%8B%9F%E6%9C%BA.md) + --- diff --git "a/OperatingSystem/8.\350\231\232\346\213\237\346\234\272.md" "b/OperatingSystem/8.\350\231\232\346\213\237\346\234\272.md" index 73d02145..0945dd88 100644 --- "a/OperatingSystem/8.\350\231\232\346\213\237\346\234\272.md" +++ "b/OperatingSystem/8.\350\231\232\346\213\237\346\234\272.md" @@ -52,80 +52,7 @@ DVM运行Java语言的应用和代码。标准的Java编译器将源代码(写 -## Android进程结构 - -如同传统的Linux系统一样,Android的第一个用户空间进程是init,它是所有其他进程的根。然而,Android的init启动的守护进程是不同的,这些守护进程更多的聚焦于底层细节(管理文件系统和硬件访问),而不是高层用户设施,例如调度定时任务。Android还有一层额外的进程,它们运行Dalvik的Java语言环境,负责执行系统中所有以Java实现的部分。 - - - -![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_process.png?raw=true) - - - -如上图,首先是init进程,它产生了一些底层守护进程。其中一个守护进程是zygote,它是高级Java语言进程的根。Android的init不以传统的方法运行shell,因为典型的Android设备没有本地控制台用于shell访问。作为替代,系统进程adbd监听请求shell访问的远程连接(例如通过USB),按要求为它们创建shell进程。因为Android大部分是用Java语言编写的,所以zygote守护进程以及由它启动的进程是系统的中心。由zygote启动的第一个进程称为system_server,它包含全部核心操作服务,其关键部分是电源管理、包管理、窗口管理和活动管理。 - -其他进程在需要的时候由zygote创建。这些进程中有一些是“持久的”进程,它们是基本操作系统的组成部分,例如phone进程中的电话栈,它必须保持始终运行。另外的应用程序进程将在系统运行的过程中按需创建和终止。 - -应用程序通过调用操作系统提供的库与操作系统进行交互,这些库合起来构成Android框架(Android framework)。这些库中有一些可以在进程内部执行其工作,但是许多库需要与其他进程执行进程间通信,作者通常是在system_server进程中提供服务的。 - - - -### Zygote - -Zygote是在启动时就运行在DVM上的一个进程。每当出现创建进程的请求时,Zygote就会产生一个新的DVM虚拟机。Zygote通过在内存中尽可能多的共享内容来最小化产生一个新DVM所消耗的时间。通常而言,许多应用程序都会使用核心库的类和相应的堆结构,而这些内容都是只读的。也就是说,被大部分应用程序使用的这些共享数据和类都是只读而不能改变的。因此,当Zygote加载时,就预加载和初始化了应用程序运行时可能用到的Java核心库和资源。当Zygote创建一个新的DVM时,这部分类不会被分配到新内存。Zygote只是简单地把子进程的这些内存页映射到父进程的相应位置。 - -实际上,几乎不需要更多的映射页。如果一个类被一个子进程自己的DVM改写,那么Zygote会将受影响的内存复制到子进程中。这种即写即复制的行为在使得最大化共享内存的同时,还能保证应用程序间不会相互影响,并在跨应用程序和进程的边界时保证安全性。 - - - -## Android进程模型 - - - -Linux的传统进程模型是用fork指令来创建新进程,然后用exec指令使用待运行的源码初始化该进程并开始执行。shell负责实现进程执行、创建新进程、执行所需的进程来运行shell指令。当指令结束时,进程被从Linux中移除。 - -Android使用的进程有些不同。活动管理器是Android负责正在运行的应用程序的管理的一部分。活动管理器协调新应用程序进程的启动,决定哪些应用程序能在其中运行,哪些已不再需要。 - - - -### 启动进程 - -为了启动新进程,活动管理器需要与zygote通信。活动管理器首先开始,它创建一个与zygote相连的专用接口,通过接口发送一条指令,表示它需要启动一个进程。这条指令主要描述需要创建的沙箱、新进程运行所需要的UID以及需要遵守的安全性制约。zygote需要作为根来运行:创建新进程时,它合理配置运行所需的UID,最终下放权限,将进程改为该UID。 - -![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_start_process.png?raw=true) - -上图展示了一个新进程中启动活动的流程: - -1. 某个现有进程(如应用程序启动器)调用活动管理器,发出意图,描述它想要启动的新活动。 -2. 活动管理器要求封装管理器将这个意图解析为一个明确的组件。 -3. 活动管理器判断这个应用程序的进程并未正在运行,然后向zygote请求一个具有合适UID的新锦成。 -4. zygote进行一次fork指令,克隆自己来创造一个新进程,下方权限并配置新进程的UID和沙箱,初始化该进程的Dalvik,使得Java runtime开始完全执行。例如,它需要在fork后启动垃圾收集等线程。 -5. 新进程如今是一个zygote的克隆,并运行着完全配置好的Java环境。它回调活动管理器,询问后者“我该做什么”。 -6. 活动管理器返回即将启动的应用程序的完整信息,如源码位置等。 -7. 新进程读取应用程序的源码,开始运行。 -8. 活动管理器将所有即将进行的操作发送给新进程,在此处为“启动活动X”。 -9. 新进程收到指令,启动活动,实体化合适的Java类并执行。 - -注意,当活动启动时,应用程序的进程可能正在运行了。在这种情况下,活动管理器会直接跳转到末尾,向该进程发送一条新指令,让它实体化并执行合适的组件。如果合适,这会导致一个额外的活动实例在应用程序中运行。 - - - -### 进程声明周期 - -活动管理器也负责判断何时进程不再被需要。活动管理器记录一个进程中运行的所有活动、接收器、服务以及内容提供其,据此可判断该进程的重要程度。 - -Android内核中的内存溢出强制结束指令是使用一个进程的oom_adj进行严格排序,决定哪个进程需要优先强制结束。活动管理器负责基于每个进程的状态,通过将其归类于几个主要用途,从而合理设定器oom_adj。/proc//oom_adj: - -- 取值是-17到+15,取值越高,越容易被干掉。如果是-17,则表示不能被kill -- 该设置参数的存在是为了和旧版本的内核兼容 - -![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_process_adj.png?raw=true) - -让RAM内存不足时,系统已经完成了进程的配置,使得内存溢出强制结束命令优先中止缓存(cache)类型的进程,尝试重新取得足够的所需内存,随后中止界面(home)类别、服务(service)类别,以此类推。在同一个oom_adj水平中,它将优先中止内存占用较大的进程。 - - - - +- [上一篇:7.嵌入式系统](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/7.%E5%B5%8C%E5%85%A5%E5%BC%8F%E7%B3%BB%E7%BB%9F.md) --- From 9dd95c870f9057079e458ed9f1165ce9720d4e75 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 1 Dec 2020 21:23:03 +0800 Subject: [PATCH 036/213] add android kernal --- .../ANR\345\210\206\346\236\220.md" | 66 ++ ...04\351\234\262\345\210\206\346\236\220.md" | 105 -- .../crash\345\210\206\346\236\220.md" | 126 +++ ...03\345\261\200\344\274\230\345\214\226.md" | 47 +- ...66\346\236\204\347\256\200\344\273\213.md" | 4 +- ...73\347\273\237\347\256\200\344\273\213.md" | 141 ++- ...13\344\270\216\347\272\277\347\250\213.md" | 187 +++- ...05\345\255\230\347\256\241\347\220\206.md" | 4 +- OperatingSystem/5.I:O.md | 13 + ...8.\350\231\232\346\213\237\346\234\272.md" | 37 +- ...13\351\227\264\351\200\232\344\277\241.md" | 199 ++++ ...10\346\201\257\346\234\272\345\210\266.md" | 918 ++++++++++++++++++ ...roid Framework\346\241\206\346\236\266.md" | 105 ++ ...ManagerService\347\256\200\344\273\213.md" | 148 +++ ...10\346\201\257\350\216\267\345\217\226.md" | 60 ++ ...30\345\210\266\345\237\272\347\241\200.md" | 51 + ...30\345\210\266\345\216\237\347\220\206.md" | 16 + ...ManagerService\347\256\200\344\273\213.md" | 131 +++ ...ManagerService\347\256\200\344\273\213.md" | 65 ++ ...57\345\212\250\350\277\207\347\250\213.md" | 4 + ...06\345\217\221\350\257\246\350\247\243.md" | 45 +- ...07\347\250\213\350\257\246\350\247\243.md" | 51 +- 22 files changed, 2369 insertions(+), 154 deletions(-) create mode 100644 "AdavancedPart/ANR\345\210\206\346\236\220.md" delete mode 100644 "AdavancedPart/Handler\345\257\274\350\207\264\345\206\205\345\255\230\346\263\204\351\234\262\345\210\206\346\236\220.md" create mode 100644 "AdavancedPart/crash\345\210\206\346\236\220.md" create mode 100644 "OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" create mode 100644 "OperatingSystem/AndroidKernal/2.Android\347\272\277\347\250\213\351\227\264\351\200\232\344\277\241\344\271\213Handler\346\266\210\346\201\257\346\234\272\345\210\266.md" create mode 100644 "OperatingSystem/AndroidKernal/3.Android Framework\346\241\206\346\236\266.md" create mode 100644 "OperatingSystem/AndroidKernal/4.ActivityManagerService\347\256\200\344\273\213.md" create mode 100644 "OperatingSystem/AndroidKernal/5.Android\346\266\210\346\201\257\350\216\267\345\217\226.md" create mode 100644 "OperatingSystem/AndroidKernal/6.\345\261\217\345\271\225\347\273\230\345\210\266\345\237\272\347\241\200.md" create mode 100644 "OperatingSystem/AndroidKernal/7.View\347\273\230\345\210\266\345\216\237\347\220\206.md" create mode 100644 "OperatingSystem/AndroidKernal/8.WindowManagerService\347\256\200\344\273\213.md" create mode 100644 "OperatingSystem/AndroidKernal/9.PackageManagerService\347\256\200\344\273\213.md" diff --git "a/AdavancedPart/ANR\345\210\206\346\236\220.md" "b/AdavancedPart/ANR\345\210\206\346\236\220.md" new file mode 100644 index 00000000..fc8ed472 --- /dev/null +++ "b/AdavancedPart/ANR\345\210\206\346\236\220.md" @@ -0,0 +1,66 @@ +# ANR分析 + +Application Not Responding,字面意思就是应用无响应,稍加解释就是用户的一些操作无法从应用中获取反馈 + + +Android系统中的应用被Activity Manager及Window Manager两个系统服务监控着,Android系统会在如下情况展示出ANR的对话框: +- Service Timeout:比如前台服务在20s内未执行完成;后台服务超过200没有执行 +- BroadcastQueue Timeout:比如前台广播在10s内未执行完成,后台60s +- ContentProvider Timeout:内容提供者,在publish过超时10s +- InputDispatching Timeout: 输入事件分发超时5s,包括按键和触摸事件。 + + + +ANR信息输出到traces.txt文件中 + +traces.txt文件是一个ANR记录文件,用于开发人员调试,目录位于/data/anr中,无需root权限即可通过pull命令获取,下面的命令可以将traces.txt文件拷贝到当前目录下 +adb pull /data/anr . + + +1) Thread基础信息 + +输出种包含所有的线程,取其中的一条 +"Thread-1" prio=5 tid=0x00007fde73872800 nid=0x4a03 waiting for monitor entry [0x000000011cb30000] + java.lang.Thread.State: BLOCKED (on object monitor) + at Test.rightLeft(Test.java:48) + - waiting to lock <0x00000007d56540a0> (a Test$LeftObject) + - locked <0x00000007d5656180> (a Test$RightObject) + at Test$2.run(Test.java:68) + at java.lang.Thread.run(Thread.java:745) +a) "Thread-1" prio=5 tid=0x00007fde73872800 nid=0x4a03 waiting for monitor entry [0x000000011cb30000] + +首先描述了线程名是『Thread-1』,然后prio=5表示优先级,tid表示的是线程id,nid表示native层的线程id,他们的值实际都是一个地址,后续给出了对于线程状态的描述,waiting for monitor entry [0x000000011cb30000]这里表示该线程目前处于一个等待进入临界区状态,该临界区的地址是[0x000000011cb30000] +这里对线程的描述多种多样,简单解释下上面出现的几种状态 + + waiting on condition(等待某个事件出现) + waiting for monitor entry(等待进入临界区) + runnable(正在运行) + in Object.wait(处于等待状态) + +作者:silentleaf +链接:https://www.jianshu.com/p/30c1a5ad63a3 +来源:简书 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 + + + + + + + + + + + + + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! + diff --git "a/AdavancedPart/Handler\345\257\274\350\207\264\345\206\205\345\255\230\346\263\204\351\234\262\345\210\206\346\236\220.md" "b/AdavancedPart/Handler\345\257\274\350\207\264\345\206\205\345\255\230\346\263\204\351\234\262\345\210\206\346\236\220.md" deleted file mode 100644 index d3ea4562..00000000 --- "a/AdavancedPart/Handler\345\257\274\350\207\264\345\206\205\345\255\230\346\263\204\351\234\262\345\210\206\346\236\220.md" +++ /dev/null @@ -1,105 +0,0 @@ -Handler导致内存泄露分析 -=== - -有关内存泄露请猛戳[内存泄露][1] - -```java -Handler mHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - // do something. - } -} -``` -当我们这样创建`Handler`的时候`Android Lint`会提示我们这样一个`warning: In Android, Handler classes should be static or leaks might occur.`。 - -一直以来没有仔细的去分析泄露的原因,先把主要原因列一下: -- `Android`程序第一次创建的时候,默认会创建一个`Looper`对象,`Looper`去处理`Message Queue`中的每个`Message`,主线程的`Looper`存在整个应用程序的生命周期. -- `Hanlder`在主线程创建时会关联到`Looper`的`Message Queue`,`Message`添加到消息队列中的时候`Message(排队的Message)`会持有当前`Handler`引用, -当`Looper`处理到当前消息的时候,会调用`Handler#handleMessage(Message)`.就是说在`Looper`处理这个`Message`之前, -会有一条链`MessageQueue -> Message -> Handler -> Activity`,由于它的引用导致你的`Activity`被持有引用而无法被回收 -- **在java中,no-static的内部类会隐式的持有当前类的一个引用。static的内部类则没有。** - -## 具体分析 -```java -public class SampleActivity extends Activity { - - private final Handler mHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - // do something - } - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - // 发送一个10分钟后执行的一个消息 - mHandler.postDelayed(new Runnable() { - @Override - public void run() { } - }, 600000); - - // 结束当前的Activity - finish(); -} -``` -在`finish()`的时候,该`Message`还没有被处理,`Message`持有`Handler`,`Handler`持有`Activity`,这样会导致该`Activity`不会被回收,就发生了内存泄露. - -## 解决方法 -- 通过程序逻辑来进行保护。 - - 如果`Handler`中执行的是耗时的操作,在关闭`Activity`的时候停掉你的后台线程。线程停掉了,就相当于切断了`Handler`和外部连接的线, - `Activity`自然会在合适的时候被回收。 - - 如果`Handler`是被`delay`的`Message`持有了引用,那么在`Activity`的`onDestroy()`方法要调用`Handler`的`remove*`方法,把消息对象从消息队列移除就行了。 - - 关于`Handler.remove*`方法 - - `removeCallbacks(Runnable r)` ——清除r匹配上的Message。 - - `removeC4allbacks(Runnable r, Object token)` ——清除r匹配且匹配token(Message.obj)的Message,token为空时,只匹配r。 - - `removeCallbacksAndMessages(Object token)` ——清除token匹配上的Message。 - - `removeMessages(int what)` ——按what来匹配 - - `removeMessages(int what, Object object)` ——按what来匹配 - 我们更多需要的是清除以该`Handler`为`target`的所有`Message(Callback)`就调用如下方法即可`handler.removeCallbacksAndMessages(null)`; -- 将`Handler`声明为静态类。 - 静态类不持有外部类的对象,所以你的`Activity`可以随意被回收。但是不持有`Activity`的引用,如何去操作`Activity`中的一些对象? 这里要用到弱引用 - -```java -public class MyActivity extends Activity { - private MyHandler mHandler; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mHandler = new MyHandler(this); - } - - @Override - protected void onDestroy() { - // Remove all Runnable and Message. - mHandler.removeCallbacksAndMessages(null); - super.onDestroy(); - } - - static class MyHandler extends Handler { - // WeakReference to the outer class's instance. - private WeakReference mOuter; - - public MyHandler(MyActivity activity) { - mOuter = new WeakReference(activity); - } - - @Override - public void handleMessage(Message msg) { - MyActivity outer = mOuter.get(); - if (outer != null) { - // Do something with outer as your wish. - } - } - } -} -``` - -[1]:(https://github.com/CharonChui/AndroidNote/blob/master/BasicKnowledge/%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F.md) - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file diff --git "a/AdavancedPart/crash\345\210\206\346\236\220.md" "b/AdavancedPart/crash\345\210\206\346\236\220.md" new file mode 100644 index 00000000..e3dd4ca7 --- /dev/null +++ "b/AdavancedPart/crash\345\210\206\346\236\220.md" @@ -0,0 +1,126 @@ +# crash分析 + + +## Java Crash流程 + +1、首先发生crash所在进程,在创建之初便准备好了defaultUncaughtHandler,用来处理Uncaught Exception,并输出当前crash的基本信息; +2、调用当前进程中的AMP.handleApplicationCrash;经过binder ipc机制,传递到system_server进程; +3、接下来,进入system_server进程,调用binder服务端执行AMS.handleApplicationCrash; +4、从mProcessNames查找到目标进程的ProcessRecord对象;并将进程crash信息输出到目录/data/system/dropbox; +5、执行makeAppCrashingLocked: + +创建当前用户下的crash应用的error receiver,并忽略当前应用的广播; +停止当前进程中所有activity中的WMS的冻结屏幕消息,并执行相关一些屏幕相关操作; + +6、再执行handleAppCrashLocked方法: + +当1分钟内同一进程连续crash两次时,且非persistent进程,则直接结束该应用所有activity,并杀死该进程以及同一个进程组下的所有进程。然后再恢复栈顶第一个非finishing状态的activity; +当1分钟内同一进程连续crash两次时,且persistent进程,,则只执行恢复栈顶第一个非finishing状态的activity; +当1分钟内同一进程未发生连续crash两次时,则执行结束栈顶正在运行activity的流程。 + +7、通过mUiHandler发送消息SHOW_ERROR_MSG,弹出crash对话框; +8、到此,system_server进程执行完成。回到crash进程开始执行杀掉当前进程的操作; +9、当crash进程被杀,通过binder死亡通知,告知system_server进程来执行appDiedLocked(); +10、最后,执行清理应用相关的四大组件信息。 + + + +- 剩余内存: /proc/meminfo,当系统可用内存小于MemTotal的10%时,非常容易发生OOM和大量GC。 +- PSS和RSS通过/proc/self/smap +- 虚拟内存: 获取大小/proc/self/status,获取具体的分布/proc/self/maps。 + +如果应用堆内存和设备内存比较充足,但还出现内存分配失败,则可能跟资源泄露有关。 +- 获取fd的限制数量:/proc/self/limits。一般单个进程允许打开的最大句柄个数为1024,如果超过800需将所有fd和文件名输出日志进行排查。 +- 获取线程数大小:/proc/self/status一个线程一般占2MB的虚拟内存,线程数超过400个比较危险,需要将所有tid和线程名输出到日志进行排查。 + + + + + + + +Native Crash + + 崩溃过程:native crash 时操作系统会向进程发送信号,崩溃信息会写入到 data/tombstones 下,并在 logcat 输出崩溃日志 + 定位:so 库剥离调试信息的话,只有相对位置没有具体行号,可以使用 NDK 提供的 addr2line 或 ndk-stack 来定位 + addr2line:根据有调试信息的 so 和相对位置定位实际的代码处 + ndk-stack:可以分析 tombstone 文件,得到实际的代码调用栈 + + + +## ANR + + + + +ANR排查流程 +1、Log获取 +1、抓取bugreport +adb shell bugreport > bugreport.txt +复制代码 +2、直接导出/data/anr/traces.txt文件 +adb pull /data/anr/traces.txt trace.txt +复制代码 +2、搜索“ANR in”处log关键点解读 + + +发生时间(可能会延时10-20s) + + +pid:当pid=0,说明在ANR之前,进程就被LMK杀死或出现了Crash,所以无法接受到系统的广播或者按键消息,因此会出现ANR + + +cpu负载Load: 7.58 / 6.21 / 4.83 +代表此时一分钟有平均有7.58个进程在等待 +1、5、15分钟内系统的平均负荷 +当系统负荷持续大于1.0,必须将值降下来 +当系统负荷达到5.0,表面系统有很严重的问题 + + +cpu使用率 +CPU usage from 18101ms to 0ms ago +28% 2085/system_server: 18% user + 10% kernel / faults: 8689 minor 24 major +11% 752/android.hardware.sensors@1.0-service: 4% user + 6.9% kernel / faults: 2 minor +9.8% 780/surfaceflinger: 6.2% user + 3.5% kernel / faults: 143 minor 4 major + + +上述表示Top进程的cpu占用情况。 +注意 +如果CPU使用量很少,说明主线程可能阻塞。 +3、在bugreport.txt中根据pid和发生时间搜索到阻塞的log处 +----- pid 10494 at 2019-11-18 15:28:29 ----- +复制代码 +4、往下翻找到“main”线程则可看到对应的阻塞log +"main" prio=5 tid=1 Sleeping +| group="main" sCount=1 dsCount=0 flags=1 obj=0x746bf7f0 self=0xe7c8f000 +| sysTid=10494 nice=-4 cgrp=default sched=0/0 handle=0xeb6784a4 +| state=S schedstat=( 5119636327 325064933 4204 ) utm=460 stm=51 core=4 HZ=100 +| stack=0xff575000-0xff577000 stackSize=8MB +| held mutexes= +复制代码 +上述关键字段的含义如下所示: + +tid:线程号 +sysTid:主进程线程号和进程号相同 +Waiting/Sleeping:各种线程状态 +nice:nice值越小,则优先级越高,-17~16 +schedstat:Running、Runable时间(ns)与Switch次数 +utm:该线程在用户态的执行时间(jiffies) +stm:该线程在内核态的执行时间(jiffies) +sCount:该线程被挂起的次数 +dsCount:该线程被调试器挂起的次数 +self:线程本身的地址 + +作者:jsonchao +链接:https://juejin.cn/post/6844903972587716621 +来源:掘金 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 + + + + + + + + + diff --git "a/AdavancedPart/\345\270\203\345\261\200\344\274\230\345\214\226.md" "b/AdavancedPart/\345\270\203\345\261\200\344\274\230\345\214\226.md" index b5c34641..68dd2232 100644 --- "a/AdavancedPart/\345\270\203\345\261\200\344\274\230\345\214\226.md" +++ "b/AdavancedPart/\345\270\203\345\261\200\344\274\230\345\214\226.md" @@ -1,8 +1,34 @@ 布局优化 === +布局优化的核心问题就是要解决因布局渲染性能不佳而导致应用卡顿的问题。 + + + +## 绘制原理 + +Android的绘制主要是借助CPU和GPU结合刷新机制来共同完成。 + +- CPU负责计算显示内容,包括Measure、Layout等操作,在UI绘制上的缺陷在于容易显示重复的视图组件,这样不仅带来重复的计算操作,而且会占用额外的GPU资源。 +- GPU负责光栅化,将UI元素绘制到屏幕上。 + +例如,文字首先要经过CPU换算成纹理,然后再传递给GPU进行渲染。而图片是先经过CPU计算,然后加载到内存中,最后再传给GPU进行渲染。 + + +## 耗时原因 +分析完布局的加载流程之后,我们发现有如下四点可能会导致布局卡顿: + +1. 首先,系统会将我们的Xml文件通过IO的方式映射的方式加载到我们的内存当中,而IO的过程可能会导致卡顿。 +2. 其次,布局加载的过程是一个反射的过程,而反射的过程也会可能会导致卡顿。 +3. 同时,这个布局的层级如果比较深,那么进行布局遍历的过程就会比较耗时。 +4. 最后,不合理的嵌套RelativeLayout布局也会导致重绘的次数过多。 + + +## 优化方式 + - 去除不必要的嵌套和节点 这是最基本的一条,但也是最不好做到的一条,往往不注意的时候难免会一些嵌套等。 + - 首次不需要的节点设置为`GONE`或使用`ViewStud`. - 使用`Relativelayout`代替`LinearLayout`. 平时写布局的时候要多注意,写完后可以通过`Hierarchy Viewer`或在手机上通过开发者选项中的显示布局边界来查看是否有不必要的嵌套。 @@ -146,7 +172,26 @@ - 减少不必要的`Inflate` 如上一步中`stub.infalte()`后将该`View`进行记录或者是`ListView`中`item inflate`的时候。 - + +- 使用ConstraintLayout降低布局嵌套层级 + + - 实现几乎完全扁平化的布局 + - 构建复杂布局性能更高 + - 具有RelativeLayout和LinearLayout的特性 + +- 使用AsyncLayoutInflater异步加载对应的布局 + + - 工作线程加载布局 + - 回调主线程 + - 节省主线程时间 + + AsyncLayoutInflater是通过侧面缓解的方式去缓解布局加载过程中的卡顿,但是它依然存在一些问题: + + - 1、不能设置LayoutInflater.Factory,需要通过自定义AsyncLayoutInflater的方式解决,由于它是一个final,所以需要将代码直接拷处进行修改。 + - 2、因为是异步加载,所以需要注意在布局加载过程中不能有依赖于主线程的操作。 + + + --- - 邮箱 :charon.chui@gmail.com diff --git "a/Architect/1.\346\236\266\346\236\204\347\256\200\344\273\213.md" "b/Architect/1.\346\236\266\346\236\204\347\256\200\344\273\213.md" index f75f78d3..fe4d4cdc 100644 --- "a/Architect/1.\346\236\266\346\236\204\347\256\200\344\273\213.md" +++ "b/Architect/1.\346\236\266\346\236\204\347\256\200\344\273\213.md" @@ -21,7 +21,7 @@ #### 规划 -规划是系统架构中最重要的组成部分,是个人或者组织制定的比较全民长远的发展计划,是对未来整体性、长期性、基本性问题的思考和考量。设计未来整套行动的方案。很早就有规划这个概念了,例如:国家的十一五规划等。当然软件开发也和生活紧密联系,一个大型的系统也需要良好的规划,规划可以说是基石,是系统架构的前提。 +规划是系统架构中最重要的组成部分,是个人或者组织制定的比较全面长远的发展计划,是对未来整体性、长期性、基本性问题的思考和考量。设计未来整套行动的方案。很早就有规划这个概念了,例如:国家的十一五规划等。当然软件开发也和生活紧密联系,一个大型的系统也需要良好的规划,规划可以说是基石,是系统架构的前提。 系统架构虽然是软件系统的结构,行为,属性的高级抽象,但其根本就是在需求分析的基础行为下,制定技术框架,对需求的技术实现。 @@ -29,7 +29,7 @@ - +​ --- - 邮箱 :charon.chui@gmail.com - Good Luck! diff --git "a/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" index 1129070f..09bff58c 100644 --- "a/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" +++ "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" @@ -104,13 +104,45 @@ 然后,操作系统询问BIOS,以获得配置信息。对于每种设备,系统检查对应的设备驱动程序是否存在。如果没有,系统要求用户插入含有该驱动程序的CD-ROM(由设备供应商提供)或者从网络上下载驱动程序。一旦有了全部的设备驱动程序,操作系统就将它们调入内核。然后初始化有关表格,创建需要的任何背景进程,并在每个终端上启动登陆程序或GUI。 -如果是从普通硬盘启动,可简单列举为如下步骤: -1. X86 PC开机时CPU处于实模式,开机时会将cs=0xffff,ip=0x0000 -2. 寻址0xFFFF0(ROM BIOS映射区) -3. 检查RAM、键盘、显示器、磁盘、主板等硬件 -4. 将磁盘0磁道0扇区(操作系统引导扇区)读入0x7c00处 -5. 设置 cs=0x07c0,ip=0x0000开始执行 + +不同的处理器和硬解系统可能会采用不同的策略,但是通用系统的启动分为三个步骤: + +- 开机并执行bootloader程序 + + 开机就是给系统开始供电,此时硬件电路会产生一个确定的复位时序,保证CPU是最后一个被复位的器件。为什么CPU要最后被复位呢? 因为,如果CPU是第一个被复位,则当CPU复位后开始运行时,其他硬件内部的寄存器状态可能还没有准备好,比如磁盘或者内存,那这样就可能出现外围硬件初始化的错误。当正确完成复位后,CPU开始执行第一条指令,该指令所在的内存地址是固定的,这由CPU的制造者指定。不同的CPU可能会从不同的地址获取指令,但这个地址必须是固定的,这个固定的地址所保存的程序往往被称为“引导程序“(Bootloader),因为其作用是装载真正的用户程序。 + + 至于如何装载,则是一个策略问题,不同的CPU会提供不同的装载方式,比如有的是通过普通的并口存储器,有的则是通过SD卡,但是无论硬件上使用何种接口装载,装载过程必须提供以下信息,具体包括: + + - 从哪里读取用户程序? + - 用户程序的长度是多少? + - 装载完用户程序后,应该跳转到哪里,即用户程序的执行入口在哪里? + +- 操作系统内核初始化 + + 执行内核程序,这一步所说的内核程序在上一步中指的就是”用户程序“。因为从CPU的角度来看,除了Bootloader之外的所有程序都是用户程序,只是从软件的角度来看,用户程序被分为”内核程序“和”应用程序“,而本步执行的是内核程序。 + + 内核程序初始化时执行的操作包括,初始化各种硬件,包括内存、网络接口、显示器、输入设备、然后建立各种内部数据结构,这些数据结构将用于多线程调度及内存的管理等。当内核初始化完毕后就开始运行具体的应用程序了。在一般情况下,习惯于将第一个应用程序称为”Home程序“。 + +- 执行第一个应用程序 + + 运行Home程序,比如Windows系统的桌面。之所以称为Home程序,是因为通过该程序可以方便的启动其他应用程序。而传统的Linux系统启动后第一个运行的程序一般是一个Terminal。 + + + +### Android系统启动过程 + +目前的Android系统大多运行在ARM处理器之上。ARM本身是一个公司的名称,从技术的角度来看,它又是一种微处理器内核的架构。 + +对于ARM处理器,当复位完毕后,处理器首先还行其片上ROM中的一小块程序。这块ROM的大小一般只有几KB,改段程序就是Bootloader程序,这段程序执行时会根据处理器上一些特定引脚的高低电平状态,选择从何种物理接口上装载用户程序,比如USB口、SD卡、并口Flash等。 + +多数基于ARM的实际硬件系统,会从并口NAND Flash芯片上的0x00000000地址处装载程序。对于一些小型嵌入式系统而言,该地址中的程序就是最终要执行的用户程序;而对于Android而言,该地址中的程序还不是Android程序,而是一个叫做uboot或者fastboot的程序,其作用是初始化硬件设备,比如网口、SDRAM、RS232等,并提供一些调试功能,比如向NAND Flash中写入新的数据,这可用于开发过程中的内核烧写、升级等。 + +当uboot(fastboot)被装载后便开始运行,它一般会先检测用户是否按下了某些特别的按键,这些特别按键是uboot在编译时预先预定好的,用于进入调试模式。如果用户没有按这些特殊的按键,则uboot会从NAND Flash中装载Linux内核,装载的地址是在编译uboot时预先约定好的。 + +Linux内核被装载后,就开始进行内核初始化的过程。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_power_on_start.png) @@ -124,11 +156,11 @@ CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从 使用高级语言编写的程序会在编译后转化成机器语言,然后通过CPU内部的寄存器来处理。不同类型的CPU,其内部寄存器的数量,种类以及寄存器存储的数值范围都是不同的。根据功能的不同,我们可以将寄存器大致划分为八类。 -***累加寄存器***:存储执行运算的数据和运算后的数据。 +***累加寄存器简称累加器(Accumulator, AC)***:是一个通用寄存器。存储临时的执行运算的数据和运算后的数据。 ***标志寄存器***:存储运算处理后的CPU的状态。 -***程序计数器***:存储下一条指令所在内存的地址。 +***程序计数器(Program Counter, PC)***:存储下一条指令所在内存的地址。 ***基址寄存器***:存储数据内存的起始地址。 @@ -136,7 +168,7 @@ CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从 ***通用寄存器***:存储任意数据。 -***指令寄存器***:存储指令。CPU内部使用,程序员无法通过程序对该寄存器进行读写操作。 +***指令寄存器(Instruction Register, IR)***:存储指令,CPU取到的指令存放在处理器的一个寄存器中,这个寄存器就是指令寄存器。CPU内部使用,程序员无法通过程序对该寄存器进行读写操作。 ***栈寄存器***:存储栈区域的起始地址。 @@ -171,6 +203,69 @@ CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从 +假设目前一台机器的处理器包含一个累加器(AC)的数据寄存器,所有指令和数据长度均为16位,使用16位的单元或字来组织存储器。指令格式中有4位是操作码。操作码定义了处理器执行的操作。通过指令格式剩下的12位,来直接访问存储器。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/cpu_zhiling_1.png) + +如上图中的三个指令中的操作码描述了要执行的操作。 + +下图描述了程序的执行过程。给出的程序片段把地址为940的存储单元中的内容与地址为941的存储单元的内容相加,并将结果保存在后一个单元中。这需要三条指令。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/cpu_process_demo.png) + +该图中,为把地址为940的存储单元中的内容与地址为941的存储单元中的内容相加,一共需要三个指令周期,每个指令周期都包含一个取指阶段和一个执行阶段。具体步骤为 : + +1. PC中包含第一条指令的地址为300,该指令内容(值为十六进制数1940)被送入指令寄存器IR中,PC增加1。注意,该处理过程中使用了存储器地址寄存器(MAR)和存储器缓冲寄存器(MBR)。为简单起见,这里未显示这些中间寄存器。 +2. IR中取出操作码,也就是最初的4位(对应到这里的十六进制,就是第一位数也就是1),而在上面1对应的二进制操作码是0001 = 从内存中载入AC。剩下的12位(后三个十六进制数)表示的是地址为940。所以这里的意思就是将存储器940地址的单元加载到AC中。 +3. 然后继续去下一条指令,PC现在是301了,所以要从地址为301的存储单元中取下一条指令,也就是(5941),同时PC增加1。这条指令是5941,最初的4位操作码是5,对应的二进制是0101,上面的操作码图标中0101 = 从内存中添加到AC。剩下的12位(后三个十六进制数)表示的是地址为941。 +4. 内存地址为941的存储单元中的内容与AC中以前的内容相加,结果保存在AC中。 +5. 接着读下一个指令,现在PC是302了,从302的存储单元中读取下一个指令为2941,PC同时加1.前四位操作码对应的是2,转换成二进制也就是0010 = 将AC存储到内存。后十二位对应的地址是941。所以这条指令的意思就是将AC中的内容存储到地址为941的存储单元中。 +6. 执行指令,将AC中的内容存储到地址为941的存储单元中。现在941存储单元的内容变成了5. + + + +### 中断 + + + + + +所有计算机都提供了允许其他模块(I/O、存储器)中断处理器正常处理过程的机制。中断最初是用于提高处理器效率的一种手段。例如,多数I/O设备都要远慢于处理器,处理器必须暂停并保持空闲,直到打印机完成工作。暂停的时间长度可能相当于成百上千哥不涉及存储器的指令周期,显然,这对于处理器的使用来说是非常浪费的。这种只有一个单独程序的情况,称为单道程序设计。在单道程序设计中处理器话费一定的运行时间进行计算,直到遇到一个I/O指令,这时它必须等到该I/O指令结束后才能继续执行。这种问题是可以避免的,就是存储器可以保存多个程序,在一个程序等待时通过切换去执行其他的程序,这种处理称为多道程序设计或多任务处理。它是现代操作系统的主要方案。多道程序设计的目的是为了让处理器和I/O设备(包括存储设备)同时报出忙状态,以实现最大的效率。 + + + +利用中断功能,处理器可以在I/O操作的执行过程中去执行其他命令。在这期间,如果I/O操作已经完成,此时外部设备在做好服务的准备后,即它准备好从处理器接收更多的数据时,外部设备的I/O模块给处理器发送一个中断请求信号。这时处理器会做出相应,暂停当前程序的处理,转去处理服务于特定I/O设备的程序,这种程序被称为中断处理程序(interupt handler)。在对该设备的服务响应完成后,处理器恢复原来的执行。 + +从用户程序的角度来看,中断打断了正常执行的序列。中断处理完成后,再回复执行。因此,用户程序并不需要为中断添加任何特殊的代码,处理器和操作系统负责挂起用户程序,然后在同一个地方恢复执行。 + +为使用中断产生的情况,在指令周期中要增加一个中断阶段。在中断阶段,处理器检查是否有中断发生,即检查是否出现中断信号。若没有中断,处理器继续运行,并在取指周期取当前程序的下一条指令。若有中断,处理器挂起当前程序的执行,并执行一个中断处理程序。这个中断处理程序通常是操作系统的一部分,它确定中断的性质,并执行所需要的操作。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/cpu_zhiling_intercept.png) + + + +### 中断处理 + + + + + +当I/O设备完成一次I/O操作时,发生以下硬件事件: + +1. 设备给处理器发送一个中断信号。 +2. 处理器在响应中断前结束当前指令的执行。 +3. 处理器对中断进行测试,确定存在未响应的中断,并给提交中断的设备发送确认信号,确认信号允许该设备取消它的中断信号。 +4. 处理器需要准备把控制权转交给中断程序。首先,需要保存从中断点恢复当前程序所需要的信息,要求的最少信息包括程序状态字(PSW)和保存在程序计数器(PC)中的下一条要执行的指令地址,它们被压入系统控制栈。 +5. 处理器把相应此中断的中断处理程序入口地址装入程序计数器。每类中断可由一个中断处理程序,具体取决于计算机系统架构和操作系统的设计。如果有多个中断程序,这一信息可能已包含在最初的中断信号中,否则处理器必须给发中断的设备发送请求,以获取含有所需信息的响应。一旦装入程序计数器,处理器就继续执行下一个指令周期,该指令周期也从取指开始。由于取指是由程序计数器的内容决定的,因此控制权被转交给中断处理程序,该程序会引起以下操作: +6. 在这一点,与被中断程序相关的程序计数器和PSW被保存到系统栈中,此外,还有一些其他信息被当做正在执行程序的状态的一部分。特别需要保存处理器寄存器的内容,因为中断处理程序可能会用到这些寄存器,因此所有这些值和任何其他状态信息都需要保存。 +7. 中断处理程序现在可以开始处理中断,其中包括检查与I/O操作相关的状态信息或其他引起中断的事件,还可能包括给I/O设备发送附加命令或应答。 +8. 中断处理结束后,被保存的寄存器值从栈中释放并恢复到寄存器中。 +9. 最后的操作是从栈中恢复PSW和程序计数器的值,因此下一条要执行的指令来自前面被中断的程序。 + + + + + ## 存储器 在任何一种计算机中,第二种主要部件都是存储器。在理想情况下,存储器应该极为迅速(快于执行一条指令,这样CPU就不会受到存储器的限制),充分大并且非常便宜。但是目前的技术无法同时满足这三个目标。 @@ -264,13 +359,41 @@ CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从 +## Linux系统 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/linux_archi.png) + + + +## Android系统 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_system_2.png) + + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/![](https://raw.githubusercontent.com/CharonChui/Pictures/master/rtsp_rtp_rtcp.jpeg)) + + +- 应用和框架:应用开发者最关心这一层及访问低层服务的API。 +- Binder IPC:Binder进程间通信机制允许应用框架打破进程的界限来访问Android系统服务代码,从而允许系统的高层框架API与Android的系统服务进行交互。 +- Android系统服务:框架中大部分能够调用系统服务的接口都向开发者开放,以便开发者能够使用底层的硬件和内核功能。Android系统服务分为两部分:媒体服务处理播放和录制媒体文件,系统服务处理应用所需要的系统功能。 +- 硬件抽象层(HAL):HAL提供调用核心层设备驱动的标准接口,以便上层代码不需要关心具体驱动和硬件的实现袭击,Android的HAL与标准的HAL基本一致。 +- Linux内核:Linux内核已被裁剪到满足移动环境的需求。 +Android在Linux内核中增加了两个提升电源管理能力的新功能: 报警和唤醒锁。 +- 报警功能是在Linux内核中实现的,开发者可通过调用运行库中的报警管理器来进行操作。通过报警管理器,应用可以请求定时叫醒服务。报警管理器是内核服务,目的是让应用即使在系统休眠的情况下也能触发警告提醒。这就使得系统随时可以进入休眠状态以节省电能,即使有一个进程有需要被唤醒的服务。 +- 唤醒锁也可以阻止Android系统进入休眠模式。一个应用程序占有一下唤醒锁中的一个: + - full_wake_lock:处理器工作,屏幕亮,键盘亮。 + - partial_wake_lock:处理器工作,屏幕关,键盘关。 + - screen_dim_wake_lock:处理工作,屏幕暗,键盘关。 + - screen_bright_wake_lock:处理器工作,屏幕亮,键盘关。 +当应用要求被管理的外设保持供电时,会通过API请求对应的锁。若无唤醒锁存在,系统就会锁定并关闭设备以节省电能。 diff --git "a/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" index 316d0c47..83bfc7f2 100644 --- "a/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" +++ "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" @@ -22,7 +22,11 @@ 最后一部分是根本。执行上下文(execution context)又称为进程状态(process state),是操作系统用来管理和控制进程所需的内部数据。这种内部信息和进程是分开的,因为操作系统信息不允许被进程直接访问。上下文包括操作系统管理进程及处理器正确执行进程所需的所有信息,包括各种处理器寄存器的内容,如程序计数器和数据寄存器。它还包括操作系统使用的信息,如进程优先级及进程是否在等待I/O事件的完成。 +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/cpu_process.png) +上图是一种进程管理方法。两个进程A和B存在与内存中的某些部分,给每个进程(包含程序、数据和上下文信息)分配了一块存储器区域,并且在由操作系统建立和维护的进程表中进行了记录。进程表包含记录每个进程的表项,表项内容包括指向包含进程的存储块地址的指针,还包括该进程的部分和全部上下文。执行上下文的其余部分存放在别处,可能和进程本身存在一起,通常还可能保存在内存中的一块独立区域。进程索引寄存器(process index register)包含当前正在控制处理器的进程在进程表中的索引。程序计数器(program counter)指向该进程中下一条待执行的指令。基址寄存器中保存该存储器区域的开始地址。 + +图中表示,进程索引寄存器表明进程B正在执行。以前执行的进程被临时中断,在A中断的同事,所有寄存器的内容被记录在其执行上下文环境中,以后操作系统就可以执行进程切换。恢复进程A的执行。进程切换过程中包括保存B的上下文和恢复A的上下文。在程序计数器中载入指向A的程序区域的值时,进程A自动恢复执行。 在任何多道程序设计系统中,CPU由一个进程快速切换至另一个进程,使每个进程各运行几十或几百毫秒。严格来说,在某一个瞬间,CPU只能运行一个进程。但在1秒钟内,它可能运行多个进程,这样就产生并行的错觉。CPU在各进程之间来回切换,这种快速的切换称为多道程序设计。 @@ -68,6 +72,14 @@ - 新建态:刚刚创建的进程,操作系统还未把他加入可执行进程组,它通常是进程控制块已经创建但还未加载到内存中的新进程 - 退出态:操作系统从可执行进程组中释放出的进程,要么它自身已停止,要么它因某种原因被取消 +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/process_state.png) + +### 挂起态 + +当内存中的所有进程都处于堵塞态时,操作系统可把其中的一个进程置为挂起态,并将它移到磁盘。此时内存所释放的空间就可被调入的另一个进程使用。操作系统执行换出操作后,将进程取到内存中的方式有两种:接纳一个新近创建的进程,或调入一个此前挂起的进程。显然,操作系统更倾向于调入一个此前挂起的进程,并为它服务,而非增加系统的总负载数。 + +挂起进程等价于不在内存中的进程。不在内存中的进程,不论他是否在等待一个时间,都不能立即执行。 + ### 进程由哪几部分组成? 进程是程序的一次运行过程,它是由程序段、数据段和进程控制块 PCB 组成的一个实体,其中: @@ -161,23 +173,23 @@ 进程间的信息交换,具体内容分为:控制信息交换和数据交换,控制信息的交换为低级通信,数据的交换为高级通信。 +- 信号 - -高级通信方式 + 基本原来是:两个或多个进程可以通过简单的信号进行合作,可以强迫一个进程在某个位置停止,直到它接收到一个特定的信号。 - 共享存储系统 -多台服务器访问同一个存储设备的同一分区 + 多台服务器访问同一个存储设备的同一分区 - 消息传递系统 -进程与其它的进程进行通信而不必借助共享数据,通过互相发送和接收消息,建立一条通信链路。 + 进程与其它的进程进行通信而不必借助共享数据,通过互相发送和接收消息,建立一条通信链路。 - 管道通信 -发送进程以字符流形式将大量数据送入管道,接收进程可从管道接收数据,二者利用管道进行通信,管道是单向的、先进先出的,它把一个进程的输出和另一个进程的输入连接在一起。一个进程(写进程)在管道的尾部写入数据,另一个进程(读进程)从管道的头部读出数据。每次只有一个进程能够真正地进入管道,其他的只能等待。 + 发送进程以字符流形式将大量数据送入管道,接收进程可从管道接收数据,二者利用管道进行通信,管道是单向的、先进先出的,它把一个进程的输出和另一个进程的输入连接在一起。一个进程(写进程)在管道的尾部写入数据,另一个进程(读进程)从管道的头部读出数据。每次只有一个进程能够真正地进入管道,其他的只能等待。 -管道分为无名管道和命名管道,前者用于父子进程通信,后者用于任意进程通信。 + 管道分为无名管道和命名管道,前者用于父子进程通信,后者用于任意进程通信。 @@ -230,10 +242,17 @@ ### 产生条件 -- 互斥条件 -- 请求保持 -- 不可剥夺 -- 环路条件 +死锁有三个必要条件: + +- 互斥。一次只有一个进程可以使用一个资源。其他进程不能访问已分配给其他进程的资源。 +- 占有且等待。当一个进程等待其他进程时,继续占有已分配的资源。 +- 不可抢占。不能强行抢占进程已占有的资源。 + +前三个条件都只是死锁存在的必要条件而非充分条件。要产生死锁,还需要第四个条件: + +- 循环等到。存在一个闭合的进程链,每个进程至少占有此链中下一个进程所需的一个资源。 + + ### 解决方法 @@ -244,6 +263,51 @@ +## Linux并发机制 + +Linux为进程间通信和同步提供了各种机制。这里只是几种。 + +- 管道 + + 管道是一个环形缓冲区,它允许两个进程以生产者/消费者的模型进行通信。因此,这是一个先进先出的队列,由一个进程写,由另一个进程度。 + +- 消息 + + 每个进程都有一个与之相关联的消息队列。当程序视图给一个满队列发送信息时,它会被堵塞。当进程视图从一个空队列读取消息时也回被堵塞。 + +- 共享内存 + + 这是虚存中由多个进程共享的一个公共内存块。进程读写共享内存所用的机器指令,与读写虚存空间的其他部分所用的指令相同。每个进程有一个只读或读写的权限。互斥约束不属于共享内存机制的一部分,但必须由使用共享内存的进程提供。 + +- 信号量 + + 信号量实际上是以集合的形式创建的,一个信号量集合中有一个或多个信号量。内核自动完成所有需要的操作,在所有操作完成前,任何其他进程都不能访问信号量。 + + 信号量由如下元素组成: + + - 信号量的当前值 + - 在信号量上操作的最后一个进程的进程ID + - 等待该信号量的值大于当前值的进程数 + - 等待该信号量的值为零的进程数 + +- 信号 + + 信号是用于向一个进程通知发生异步事件的机制。信号类似于硬件中断,但没有优先级,即内核公平地对待所有的信号。 + +- 自旋锁 + + 在Linux中保护临界区的常用技术是自旋锁。在同一个时刻,只有一个线程能够获得自旋锁。其他任何试图获得自旋锁的线程将一直进行尝试(即自旋),知道获得了该锁。 + + + +### Android进程间通信 + +虽然Linux内核包含很多用于进程间通信(IPC)的机制,但是Adnroid系统在IPC中仍然新增了一个连接器。连接器提供了一个轻量级的远程程序调用功能,它在内存和事务处理方面非常高效,非常适合嵌入式系统。 + +连接器被用来传递两个进程之间的交互。进程(客户端)组件发起一个调用,调用直接传递给位于内核的连接器,连接器将其传递给目标进程(服务器端)的目标组件,目标进程返回的结果通过连接器传递给发起调用的进程组件。 + + + ## Android进程结构 如同传统的Linux系统一样,Android的第一个用户空间进程是init,它是所有其他进程的根。然而,Android的init启动的守护进程是不同的,这些守护进程更多的聚焦于底层细节(管理文件系统和硬件访问),而不是高层用户设施,例如调度定时任务。Android还有一层额外的进程,它们运行Dalvik的Java语言环境,负责执行系统中所有以Java实现的部分。 @@ -254,7 +318,7 @@ -如上图,首先是init进程,它产生了一些底层守护进程。其中一个守护进程是zygote,它是高级Java语言进程的根。Android的init不以传统的方法运行shell,因为典型的Android设备没有本地控制台用于shell访问。作为替代,系统进程adbd监听请求shell访问的远程连接(例如通过USB),按要求为它们创建shell进程。因为Android大部分是用Java语言编写的,所以zygote守护进程以及由它启动的进程是系统的中心。由zygote启动的第一个进程称为system_server,它包含全部核心操作服务,其关键部分是电源管理、包管理、窗口管理和活动管理。 +如上图,首先是init进程,它产生了一些底层守护进程,init进程是所有用户进程的鼻祖。其中一个守护进程是zygote,它是高级Java语言进程的根。Android的init不以传统的方法运行shell,因为典型的Android设备没有本地控制台用于shell访问。作为替代,系统进程adbd监听请求shell访问的远程连接(例如通过USB),按要求为它们创建shell进程。因为Android大部分是用Java语言编写的,所以zygote守护进程以及由它启动的进程是系统的中心。由zygote启动的第一个进程称为system_server,它包含全部核心操作服务,其关键部分是电源管理、包管理、窗口管理和活动管理。 其他进程在需要的时候由zygote创建。这些进程中有一些是“持久的”进程,它们是基本操作系统的组成部分,例如phone进程中的电话栈,它必须保持始终运行。另外的应用程序进程将在系统运行的过程中按需创建和终止。 @@ -264,10 +328,65 @@ ### Zygote -Zygote是在启动时就运行在DVM上的一个进程。每当出现创建进程的请求时,Zygote就会产生一个新的DVM虚拟机。Zygote通过在内存中尽可能多的共享内容来最小化产生一个新DVM所消耗的时间。通常而言,许多应用程序都会使用核心库的类和相应的堆结构,而这些内容都是只读的。也就是说,被大部分应用程序使用的这些共享数据和类都是只读而不能改变的。因此,当Zygote加载时,就预加载和初始化了应用程序运行时可能用到的Java核心库和资源。当Zygote创建一个新的DVM时,这部分类不会被分配到新内存。Zygote只是简单地把子进程的这些内存页映射到父进程的相应位置。 +系统中运行的第一个Dalvik虚拟机程序叫做zygote,该名称的意义是“一个卵”,因为接下来的所有Dalvik虚拟机进程都是通过这个卵孵化出来的。Zygote是在启动时就运行在DVM上的一个进程。 + +zygote进程汇总包含两个主要模块,分别如下: + +- Socket服务端:该Socket服务端用于接收启动新的Dalvik进程的命令。 +- Framework共享类及共享资源。当zygote进程启动后,会装载一些共享的类及资源,其中共享类是在preload-classes文件中被定义,共享资源是在preload-resources中被定义。因为zygote进程用于孵化出其他Dalvik进程,因此,这些类和资源装载后,新的Dalvik进程就不需要再装载这些类和资源了,这也就是所谓的共享。 + + + +每当出现创建进程的请求时,Zygote就会产生一个新的DVM虚拟机。Zygote通过在内存中尽可能多的共享内容来最小化产生一个新DVM所消耗的时间。通常而言,许多应用程序都会使用核心库的类和相应的堆结构,而这些内容都是只读的。也就是说,被大部分应用程序使用的这些共享数据和类都是只读而不能改变的。因此,当Zygote加载时,就预加载和初始化了应用程序运行时可能用到的Java核心库和资源。当Zygote创建一个新的DVM时,这部分类不会被分配到新内存。Zygote只是简单地把子进程的这些内存页映射到父进程的相应位置。 实际上,几乎不需要更多的映射页。如果一个类被一个子进程自己的DVM改写,那么Zygote会将受影响的内存复制到子进程中。这种即写即复制的行为在使得最大化共享内存的同时,还能保证应用程序间不会相互影响,并在跨应用程序和进程的边界时保证安全性。 +zygote进程对应的具体程序是app_process,该程序存在于system/bin目录下,启动该程序的指令是在init.rc中进行配置的。 + + + + + +#### System Server进程 + +System Server进程是由zygote进程fork而来,System Server是zygote孵化的第一个Dalvik进程,SystemServer仅仅是该进程的别名,而该进程具体对应的程序依然是app_process,因为System是从app_process中孵化出来的。System Server负责启动和管理整个Java framework,SystemServer进程在Android的运行环境中扮演了“神经中枢”的作用,APK应用中能够直接交互的大部分系统服务都在该进程中运行,常见的有WindowManagerServer(WmS)、ActivityManagerSystemService(AmS)、PackageManagerServer(PmS)等,这些系统服务都是以一个线程的方式存在于SystemServer进程中。SystemServer的main()函数会首先创建一个ServerThread对象,该对象是一个线程,然后直接运行该线程。而在ServerThread的run()方法内部真正启动各种服务线程,都有: + +- EntropyService:提供伪随机数 +- PowerManagerService:电源管理服务 +- ActivityManagerService:最核心的服务之一,管理Activity +- TelephonyRegistry:通过该服务注册电话模块的事件响应,比如重启、关闭、启动等 +- PackageManagerService:程序包管理服务 +- AccountManagerService:账户管理服务,是指联系人账户,而不是Linux系统账户 +- ContentService:ContentProvider服务,提供跨进程数据交互 +- BatteryService:电池管理服务 +- LightsService:自然光强度感应传感器服务 +- VibratorService:振动器服务 +- AlarmManagerService:定时器管理服务,提供定时提醒服务 +- WindowManagerService:Framework最核心的服务之一,负责窗口管理 +- BluetoothService:蓝牙服务 +- DevicePolicyManagerService:提供一些系统级别的设置及属性 +- StatusBarManagerService:状态栏管理服务 +- ClipboardService:系统剪切板服务 +- InputMethodManagerService:输入法管理服务 +- NetStatService:网络状态服务 +- NetworkManagementService:网络管理服务 +- ConnectivityService:网络连接管理服务 +- NotificationManagerService:通知栏管理服务 +- LocationManagerService:地理位置服务 +- AudioService:音频管理服务 +- .... + +SystemServer中创建了一个Socket客户端,并有AmS负责管理该客户端,之后所有的Dalvik进程都将通过该Socket客户端间接被启动。当需要启动新的APK进程时,AmS中会通过该Socket客户端向zygote进程的Socket服务端发送一个启动命令,然后zygote会孵化出新的进程。 + + + + + +从系统架构的角度来看,先创建一个zygote并加载共享类的资源,然后通过该zygote去孵化新的Dalvik进程,该架构的特点有两个: + +- 每一个进程都是一个Dalvik虚拟机,而Dalvik虚拟机是一个类似于Java虚拟机的程序,并且从开发的过程来看,与标准的Java程序开发基本一致。因此对于程序员来讲,必须要学习新的语言,并可以使用Java程序在过去几十年中已经成熟的各种类库资源。 +- zygote进程预先会装载共享类和共享资源,这些类及资源实际上就是SDK中定义的大部分类和资源,因此,当通过zygote孵化出新的进程后,新的APK进程只需要去装载APK自身包含的类和资源即可,这就有效的解决了多个APK共享Framework资源的问题。 + ## Android进程模型 @@ -302,6 +421,8 @@ Android使用的进程有些不同。活动管理器是Android负责正在运行 + + ### 进程生命周期 活动管理器也负责判断何时进程不再被需要。活动管理器记录一个进程中运行的所有活动、接收器、服务以及内容提供其,据此可判断该进程的重要程度。 @@ -317,7 +438,47 @@ Android内核中的内存溢出强制结束指令是使用一个进程的oom_adj -## 进程和线程的区别 +### Android系统启动的核心流程如下: + + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/Andriod-Boot-Process.jpg) + + + +1. 启动电源以及系统启动:当电源按下时引导芯片从预定义的地方(固化在ROM)开始执行(Boot ROM),Boot ROM会去加载引导程序BootLoader到RAM,然后执行。 + +2. 引导程序BootLoader:BootLoader是在Android系统开始运行前的一个小程序,主要用于把系统OS拉起来并运行。 +3. Linux内核启动:当内核启动时,设置缓存、被保护存储器、计划列表、加载驱动。当其完成系统设置时,会先在系统文件中寻找init.rc文件,并启动init进程。 +4. init进程启动:初始化和启动属性服务,init进程会孵化出eadbd、logd、等用户守护进程,还会启动ServiceManager(binder服务管家)、botanic(开机动画)等服务,并且启动Zygote进程。 +5. Zygote进程启动:zygote进程是Android系统的第一个Java进程(即虚拟机进程),它是所有Java进程的父进程。它会创建JVM并为其注册JNI方法,创建服务器端Socket,启动SystemServer进程。并且,zygote进程在启动的时候会创建DVM或者ART。因此通过从zygote进程fork创建的应用程序进程和systemserver进程都可以在内部获取一个DVM或者ART的实例副本。它还会提前加载类preloadClasses和提前加载资源preloadResouces。 +6. SystemServer进程启动:System Server是zygote孵化的第一个进程,它会启动Binder线程池和SystemServiceManager,并且启动各种系统服务,包括ActivityManager、WindowManager、PackageManager、PowerManager等服务。 +7. Media Server进程,是由init进程fork而来,负责启动和管理整个C++ framework,包括AudioFlinger,Camera Service等服务。 +8. Launcher启动:是zygote孵化的第一个App进程,被SystemServer进程启动的AMS会启动Launcher,Launcher启动后会将已安装应用的快捷图标显示到系统桌面上。zygote还会创建Broweer、Phone、Email等App进程,每个App至少运行在一个进程上。 + + + +对于IPC(Inter-Process Communication, 进程间通信),Linux现有管道、消息队列、共享内存、套接字、信号量、信号这些IPC机制,Android额外还有Binder IPC机制,Android OS中的Zygote进程的IPC采用的是Socket机制,在上层system server、media server以及上层App之间更多的是采用Binder IPC方式来完成跨进程间的通信。对于Android上层架构中,很多时候是在同一个进程的线程之间需要相互通信,例如同一个进程的主线程与工作线程之间的通信,往往采用的Handler消息机制。 + + + +## 线程 + +多线程技术是指把执行一个应用程序的进程划分为可以同时运行的多个线程。 + + + +### 进程和线程的区别 + +进程有如下两个特点: + +- 资源所有权:进程包括存放进程映像的虚拟地址空间,进程映像是程序、数据、栈和进程控制块中定义的属性集。进程总具有对资源的控制权和所有权,这些资源包括内存、I/O通道、I/O设备和文件。操作系统能提供预防进程间发生不必要资源冲突的保护功能。 +- 调度/执行:进程执行时采用一个或多程序的执行路径,不同进程的执行过程会交替执行。因此进程具有执行态和分配给其的优先级,是可被操作系统调度和分派的实体。 + +上面这两个特点是独立的,因此操作系统能分别处理它们。为了区分这两个特点,通常将分派的单位成为线程或轻量级进程,而将拥有资源所有权的单位成为进程或任务。 + +- 线程(thread):可分派的工作单元。它包括处理器上下文环境(包含程序计数器和栈指针)和栈中自身的数据区域。线程顺序执行且可以中断,因此处理器可以转到另一个线程。 +- 进程(process):一个或多个线程和相关系统资源(如包含程序和代码的存储空间、打开的文件和设备)的集合。它严格对应于一个正在执行的程序的概念。通过把一个应用程序分解成多个线程,程序员可以很大程度上控制应用程序的模块性及相关事件的时间安排。 线程比进程更轻量级,所以它们比进程更容易(更快)创建,也更容易撤销。在许多系统中,创建一个线程比创建一个进程要快10~100倍。 diff --git "a/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" "b/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" index 3a54aa0e..2490734b 100644 --- "a/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" +++ "b/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" @@ -245,11 +245,11 @@ Android包含了标准Linux内核中内存管理设施的许多扩展,具体 - ASHMem:这个功能提供匿名共享内存,它将内存抽象为文件描述符。文件描述符可以传递给另一个进程以共享内存。 - Pmen:这个功能分配虚拟内存,使的它在物理上是连续的,因此对于那些不支持虚拟内存的设备非常实用。 -- Low Memory Killer:ndorid基于oomKiller原理所扩展的一个多层次oomKiller,OOMkiller(Out Of Memory Killer)是在Linux系统无法分配新内存的时候,选择性杀掉进程,到oom的时候,系统可能已经不太稳定,而LowMemoryKiller是一种根据内存阈值级别触发的内存回收的机制,在系统可用内存较低时,就会选择性杀死进程的策略,相对OOMKiller,更加灵活。主存耗尽时,使用大量内存的应用不是让步对内存的使用就是被终结。这个功能可让系统通知应用释放内存,如果应用不配合,则终结应用。 - +- Low Memory Killer:andorid基于oomKiller原理所扩展的一个多层次oomKiller。在Android中运行了一个OOM进程,该进程启动时会首先向Linux内核中把自己注册为一个OOM Killer,即当Linux内核的内存管理模块检测到系统内存低的时候会通知已经注册的OOM进程,然后这些OOMkille会选择性杀掉进程,到oom的时候,系统可能已经不太稳定,而LowMemoryKiller是一种根据内存阈值级别触发的内存回收的机制,在系统可用内存较低时,就会选择性杀死进程的策略,相对OOMKiller,更加灵活。主存耗尽时,使用大量内存的应用不是让步对内存的使用就是被终结。这个功能可让系统通知应用释放内存,如果应用不配合,则终结应用。 +Android的底层Linux未采用磁盘虚拟内存机制,程序只能使用屋里内存作为最大内存,所以AmS中采用了自动杀死优先级较低进程的方法以达到释放内存的目的。 diff --git a/OperatingSystem/5.I:O.md b/OperatingSystem/5.I:O.md index 2a4e19c2..29367863 100644 --- a/OperatingSystem/5.I:O.md +++ b/OperatingSystem/5.I:O.md @@ -11,9 +11,22 @@ 执行I/O的三种技术: - 程序控制I/O:处理器代表一个进程给I/O模块发送一个I/O命令,该进程进入忙等待,直到操作完成才能继续执行。 + - 中断驱动I/O:处理器代表进程向I/O模块发出一个I/O命令。有两种可能性:若来自进程的I/O指令是非堵塞的,则处理器继续执行发出I/O命令的进程的后续指令。若I/O指令是堵塞的,则处理器执行的下一条指令来自操作系统,它将当前的进程设置为堵塞态并调度其他进程。 + - 直接存储器访问(DMA):一个DMA模块控制内存和I/O模块之间的数据交换。为传送一块数据,处理器给DMA模块发请求,且只有在整个数据块传送结束后,它才被中断。 + DMA技术工作流程如下: + + 如果处理器想读或写一块数据时,它通过想DMA模块发送以下信息来给DMA模块发出一条命令: + + - 请求读操作或写操作的信号,通过在处理器和DMA模块之间使用读写控制线发送。 + - 相关I/O设备地址,通过数据线发送。 + - 从存储器中读或向存储器中写的起始地址,在数据线上传送,并由DMA模块保存在其地址寄存器中。 + - 读或写的字数,也通过数据线传送,并由DMA模块保存在其数据计数寄存器中 + + 然后处理器继续执行其他工作,此时它已把这个I/O操作委托给DMA模块。DMA模块直接从存储器中或向存储器中逐字传送整块数据,并且数据不再需要通过处理器。传送结束后,DMA模块给处理器发送一个中断信号。因此,只有在传送开始和结束时才会用到处理器。 + ### 磁盘高速缓存 diff --git "a/OperatingSystem/8.\350\231\232\346\213\237\346\234\272.md" "b/OperatingSystem/8.\350\231\232\346\213\237\346\234\272.md" index 0945dd88..47f61216 100644 --- "a/OperatingSystem/8.\350\231\232\346\213\237\346\234\272.md" +++ "b/OperatingSystem/8.\350\231\232\346\213\237\346\234\272.md" @@ -30,11 +30,9 @@ Android平台的虚拟机称为Dalvik,Dalvik VM(DVM)执行格式为Dalvik Exec 虚拟机运行在Linux内核的顶部,它依赖于底层的功能(如线程和底层的内存管理)。Dalvik核心类库的目的是,为那些使用标准Java编程的人员提供熟悉的开发环境,但它是专门为满足小型移动设备的需要而设计的。 - - 每个Android应用程序都运行在自己的进程中,有自己的Dalvik运行实例。Dalvik是可以在一台设备上高效执行多个副本的虚拟机。 - +当Java程序运行时,都是由一个虚拟机来解释Java的字节码,它将这些字节码翻译成本地CPU的指令码,然后执行。对Java程序而言,负责解释并执行的就是一个虚拟机,而对于Linux而言,这个进程只是一个普通的进程,它与一个只有一行代码的Hello World可执行程序无本质区别。所以启动一个虚拟机的方法就跟启动任何一个可执行程序的方法是相同的,那就是在命令行下输入可执行程序的名称,并在参数中指定要执行的Java类。而dalvikvm的作用就是创建一个虚拟机并执行参数中指定的Java类。 @@ -50,6 +48,39 @@ DVM运行Java语言的应用和代码。标准的Java编译器将源代码(写 +### DVM示例 + +下面以一个例子来说明dalvikvm的使用方法: + +1. 首先新建一个Foo.java文件,如下: + + ```java + class Foo { + public static void main(String[] args) { + System.out.println("Hello dalvik"); + } + } + ``` + +2. 然后编译该文件,并生成jar文件,如下: + + ```java + $ javac Foo.java + $ dx --dex --output=foo.jar Foo.class + ``` + + dx工具的作用是将.class转换为dex文件,因为Dalvik虚拟机所执行的程序不是标准的Jar文件,而是将jar经过特别的转换以提高执行效率,而在转换后就是dex文件。dx工具是Android源码的一部分,其路径是在out目录下。dx执行时,--output参数用于指定jar文件的输出路径,注意该jar文件内部包含已经不是纯粹的.class文件,而是dex格式文件,jar仅仅是zip包。 + +3. 生成了该jar包后,就可以把该jar包push到设备中,并执行以下命令: + + ```java + $ adb push foo.jar /data/app + $ adb shell dalvikvm -cp /data/app/foo.jar Foo + Hello dalvik + ``` + + 以上命令首先将jar包push到/data/app目录下,因为该目录一般用于存放应用程序,接着使用adb shell执行dalvikvm程序。dalvikvm的执行语法是: dalvikvm -cp 类路径 类名,从这里可以感觉到,dalvikvm的作用就像在pc上执行java程序一样。 + - [上一篇:7.嵌入式系统](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/7.%E5%B5%8C%E5%85%A5%E5%BC%8F%E7%B3%BB%E7%BB%9F.md) diff --git "a/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" "b/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" new file mode 100644 index 00000000..0a6b51a4 --- /dev/null +++ "b/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" @@ -0,0 +1,199 @@ +# 1.Android进程间通信 + +## Binder简介 + +Binder,英文的意思是别针、回形针。我们经常用别针把两张纸”别“在一起,而在Android中,Binder用于完成进程间通信(IPC),即把多个进程”别“在一起。比如,普通应用程序可以调用音乐播放服务提供的播放、暂停、停止等功能。Binder工作在Linux层面,属于一个驱动,只是这个驱动不需要硬件,或者说其操作的硬件是基于一小段内存。从线程的角度来讲,Binder驱动代码运行在内核态,客户端程序调用Binder是通过系统调用完成的。 + + + +## Linux进程间通信 + +无论是Android系统,还是各种Linux衍生系统,各个组件、模块往往运行在各种不同的进程和线程内,这里就必然涉及进程/线程之间的通信。对于IPC(Inter-Process Communication, 进程间通信),Linux目前有一下这些IPC机制: + +- 管道:在创建时分配一个page大小的内存,缓存区大小比较有限; +- 消息队列:信息复制两次,额外的CPU消耗;不合适频繁或信息量大的通信; +- 共享内存:无须复制,共享缓冲区直接付附加到进程虚拟地址空间,速度快;但进程间的同步问题操作系统无法实现,必须各进程利用同步工具解决; +- 套接字:作为更通用的接口,传输效率低,主要用于不通机器或跨网络的通信; +- 信号量:常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。 +- 信号: 不适用于信息交换,更适用于进程中断控制,比如非法内存访问,杀死某个进程等; + +Android额外还有Binder IPC机制,Android OS中的Zygote进程的IPC采用的是Socket机制,在上层system server、media server以及上层App之间更多的是采用Binder。 IPC方式来完成跨进程间的通信。对于Android上层架构中,很多时候是在同一个进程的线程之间需要相互通信,例如同一个进程的主线程与工作线程之间的通信,往往采用的Handler消息机制。所以对于Android最上层架构而言,最常用的通信方式是: + +- Binder + +- Socket + + Socket通信方式也是C/S架构,比Binder简单很多。在Android系统中采用Socket通信方式的主要有: + + - zygote:用于孵化进程,system_server创建进程是通过socket向zygote进程发起请求; + - installd:用于安装App的守护进程,上层PackageManagerService很多实现最终都是交给它来完成; + - lmkd:lowmemorykiller的守护进程,Java层的LowMemoryKiller最终都是由lmkd来完成; + - adbd:这个也不用说,用于服务adb; + - logcatd:这个不用说,用于服务logcat; + - vold:即volume Daemon,是存储类的守护进程,用于负责如USB、Sdcard等存储设备的事件处理。 + + 等等还有很多,这里不一一列举,Socket方式更多的用于Android framework层与native层之间的通信。Socket通信方式相对于binder比较简单,这里省略。 + +- Handler + + + +## Android为什么要使用Binder + +Binder作为Android系统提供的一种IPC机制。首先一个问题就是为什么Linux已经有那么多IPC通信的机制,Android还要用Binder。 + +**接下来正面回答这个问题,从5个角度来展开对Binder的分析:** + +**(1)从性能的角度** **数据拷贝次数:**Binder数据拷贝只需要一次,而管道、消息队列、Socket都需要2次,但共享内存方式一次内存拷贝都不需要;从性能角度看,Binder性能仅次于共享内存。 + +**(2)从稳定性的角度** +Binder是基于C/S架构的,简单解释下C/S架构,是指客户端(Client)和服务端(Server)组成的架构,Client端有什么需求,直接发送给Server端去完成,架构清晰明朗,Server端与Client端相对独立,稳定性较好;而共享内存实现方式复杂,没有客户与服务端之别, 需要充分考虑到访问临界资源的并发同步问题,否则可能会出现死锁等问题;从这稳定性角度看,Binder架构优越于共享内存。 + +仅仅从以上两点,各有优劣,还不足以支撑google去采用binder的IPC机制,那么更重要的原因是: + +**(3)从安全的角度** +传统Linux IPC的接收方无法获得对方进程可靠的UID/PID,从而无法鉴别对方身份;而Android作为一个开放的开源体系,拥有非常多的开发平台,App来源甚广,因此手机的安全显得额外重要;对于普通用户,绝不希望从App商店下载偷窥隐射数据、后台造成手机耗电等等问题,传统Linux IPC无任何保护措施,完全由上层协议来确保。 + +Android为每个安装好的应用程序分配了自己的UID,故进程的UID是鉴别进程身份的重要标志,前面提到C/S架构,**Android系统中对外只暴露Client端,Client端将任务发送给Server端,Server端会根据权限控制策略,判断UID/PID是否满足访问权限,目前权限控制很多时候是通过弹出权限询问对话框,让用户选择是否运行**。Android 6.0,也称为Android M,在6.0之前的系统是在App第一次安装时,会将整个App所涉及的所有权限一次询问,只要留意看会发现很多App根本用不上通信录和短信,但在这一次性权限权限时会包含进去,让用户拒绝不得,因为拒绝后App无法正常使用,而一旦授权后,应用便可以胡作非为。 + +针对这个问题,google在Android M做了调整,不再是安装时一并询问所有权限,而是在App运行过程中,需要哪个权限再弹框询问用户是否给相应的权限,对权限做了更细地控制,让用户有了更多的可控性,但**同时也带来了另一个用户诟病的地方,那也就是权限询问的弹框的次数大幅度增多。**对于Android M平台上,有些App开发者可能会写出让手机异常频繁弹框的App,企图直到用户授权为止,这对用户来说是不能忍的,用户最后吐槽的可不光是App,还有Android系统以及手机厂商,有些用户可能就跳果粉了,这还需要广大Android开发者以及手机厂商共同努力,共同打造安全与体验俱佳的Android手机。 + +Android中权限控制策略有SELinux等多方面手段。传统IPC只能由用户在数据包里填入UID/PID;另外,可靠的身份标记只有由IPC机制本身在内核中添加。其次传统IPC访问接入点是开放的,无法建立私有通道。从安全角度,Binder的安全性更高。 + +**说到这,可能有人要反驳**,Android就算用了Binder架构,而现如今Android手机的各种流氓软件,不就是干着这种偷窥隐射,后台偷偷跑流量的事吗?没错,确实存在,但这不能说Binder的安全性不好,因为Android系统仍然是掌握主控权,可以控制这类App的流氓行为,只是对于该采用何种策略来控制,在这方面android的确存在很多有待进步的空间,这也是google以及各大手机厂商一直努力改善的地方之一。在Android 6.0,google对于app的权限问题作为较多的努力,大大收紧的应用权限;另外,在**Google举办的Android Bootcamp 2016**大会中,google也表示在Android 7.0 (也叫Android N)的权限隐私方面会进一步加强加固,比如SELinux,Memory safe language(还在research中)等等,在今年的5月18日至5月20日,google将推出Android N。 + +话题扯远了,继续说Binder。 + +**(4)从语言层面的角度** +大家多知道Linux是基于C语言(面向过程的语言),而Android是基于Java语言(面向对象的语句),而对于Binder恰恰也符合面向对象的思想,将进程间通信转化为通过对某个Binder对象的引用调用该对象的方法,而其独特之处在于Binder对象是一个可以跨进程引用的对象,它的实体位于一个进程中,而它的引用却遍布于系统的各个进程之中。可以从一个进程传给其它进程,让大家都能访问同一Server,就像将一个对象或引用赋值给另一个引用一样。Binder模糊了进程边界,淡化了进程间通信过程,整个系统仿佛运行于同一个面向对象的程序之中。从语言层面,Binder更适合基于面向对象语言的Android系统,对于Linux系统可能会有点“水土不服”。 + +**另外,Binder是为Android这类系统而生,而并非Linux社区没有想到Binder IPC机制的存在,对于Linux社区的广大开发人员,我还是表示深深佩服,让世界有了如此精湛而美妙的开源系统。**也并非Linux现有的IPC机制不够好,相反地,经过这么多优秀工程师的不断打磨,依然非常优秀,每种Linux的IPC机制都有存在的价值,同时在Android系统中也依然采用了大量Linux现有的IPC机制,根据每类IPC的原理特性,因时制宜,不同场景特性往往会采用其下最适宜的。比如在**Android OS中的Zygote进程的IPC采用的是Socket(套接字)机制**,Android中的**Kill Process采用的signal(信号)机制**等等。而**Binder更多则用在system_server进程与上层App层的IPC交互**。 + +**(5) 从公司战略的角度** + +总所周知,Linux内核是开源的系统,所开放源代码许可协议GPL保护,该协议具有“病毒式感染”的能力,怎么理解这句话呢?受GPL保护的Linux Kernel是运行在内核空间,对于上层的任何类库、服务、应用等运行在用户空间,一旦进行SysCall(系统调用),调用到底层Kernel,那么也必须遵循GPL协议。 + +而Android 之父 Andy Rubin对于GPL显然是不能接受的,为此,Google巧妙地将GPL协议控制在内核空间,将用户空间的协议采用Apache-2.0协议(允许基于Android的开发商不向社区反馈源码),同时在GPL协议与Apache-2.0之间的Lib库中采用BSD证授权方法,有效隔断了GPL的传染性,仍有较大争议,但至少目前缓解Android,让GPL止步于内核空间,这是Google在GPL Linux下 开源与商业化共存的一个成功典范。 + +**有了这些铺垫,我们再说说Binder的今世前缘** + +Binder是基于开源的 [OpenBinder](https://link.zhihu.com/?target=http%3A//www.angryredplanet.com/~hackbod/openbinder/docs/html/BinderIPCMechanism.html)实现的,OpenBinder是一个开源的系统IPC机制,最初是由 [Be Inc.](https://link.zhihu.com/?target=https%3A//en.wikipedia.org/wiki/Be_Inc.) 开发,接着由[Palm, Inc.](https://link.zhihu.com/?target=https%3A//en.wikipedia.org/wiki/Palm%2C_Inc.)公司负责开发,现在OpenBinder的作者在Google工作,既然作者在Google公司,在用户空间采用Binder 作为核心的IPC机制,再用Apache-2.0协议保护,自然而然是没什么问题,减少法律风险,以及对开发成本也大有裨益的,那么从公司战略角度,Binder也是不错的选择。 + +另外,再说一点关于OpenBinder,在2015年OpenBinder以及合入到Linux Kernel主线 3.19版本,这也算是Google对Linux的一点回馈吧。 + +**综合上述5点,可知Binder是Android系统上层进程间通信的不二选择。** + + + +**最后,简单讲讲Android Binder架构** + +Binder在Android系统中江湖地位非常之高。在Zygote孵化出system_server进程后,在system_server进程中出初始化支持整个Android framework的各种各样的Service,而这些Service从大的方向来划分,分为Java层Framework和Native Framework层(C++)的Service,几乎都是基于BInder IPC机制。 + +1. **Java framework:作为Server端继承(或间接继承)于Binder类,Client端继承(或间接继承)于BinderProxy类。**例如 ActivityManagerService(用于控制Activity、Service、进程等) 这个服务作为Server端,间接继承Binder类,而相应的ActivityManager作为Client端,间接继承于BinderProxy类。 当然还有PackageManagerService、WindowManagerService等等很多系统服务都是采用C/S架构; +2. **Native Framework层:这是C++层,作为Server端继承(或间接继承)于BBinder类,Client端继承(或间接继承)于BpBinder。**例如MediaPlayService(用于多媒体相关)作为Server端,继承于BBinder类,而相应的MediaPlay作为Client端,间接继承于BpBinder类。 + + + +**总之,一句话"无Binder不Android"。** + + + +前面人都说了Binder的优点,我来讲故事 + +1. 当年Andy Rubin有个公司 Palm 做掌上设备的 就是当年那种PDA 有个系统叫PalmOS 后来palm被收购了以后 Andy Rubin 创立了Android + +2. Palm收购过一个公司叫 Be 里面有个移动系统 叫 BeOS 进程通信自己学了个实现 叫Binder 由一个叫 Dianne Hackbod的人开发并维护 后来Binder 也被用到了 PalmOS里 + +3. Android创立了以后 Andy从Palm带走了一大批人,其中就有Dianne。Dianne成为安卓系统总架构师。 + +- 如果你是她,你会选择用a.Linux已有的进程通信手段吗? 不会,要不当年也不会搞个新东西出来 + +- 重写一个新东西 也不会 binder反正是自己写的开源库 + +- 用binder 已经被两个公司用过 而且是自己写的 可靠放心 + +我是她我就选C + +你可以看到 如果当年Dianne没有加入Be 或者Be没有被收购 ,又或者Dianne没有和Andy加入Android 那Android也不一定会用binder。 + + + +## Binder框架 + +Binder是一种架构,这种架构提供了服务端接口、Binder驱动、客户端接口三个模块。 + +- 服务端 + + 一个Binder服务端实际上就是一个Binder类的对象,该对象一旦创建,内部就启动一个隐藏线程。该线程接下来会接收Binder驱动发送的消息,收到消息后,会执行到Binder对象中的onTransact()函数,并按照该函数的参数执行不同的服务代码。因此,要实现一个Binder服务就必须重载onTransact()方法。 + + 可以想象,重载onTransact()函数的主要内容是把onTransact()函数的参数转换为服务函数的参数,而onTransact()函数的参数来源是客户端调用transact()函数时传入的,因此,如果transact()有固定格式的输入,那么onTransact()就会有固定格式的输出。 + +- Binder驱动 + + 任何一个服务端Binder对象被创建时,同时会在Binder驱动中创建一个mRemote对象,该对象的类型也是Binder类。客户端要访问远程服务时,都是通过mRemote对象。 + + ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/binder_archi.png) + +- 应用程序客户端 + + 客户端想要访问远程服务,必须获取远程服务在Binder对象中对应的mRemote引用,获得该mRemote对象后,就可以调用其transact()方法,调用该方法后,客户端线程进入Binder驱动,Binder驱动就会挂起当前线程,并向远程服务发送一个消息,消息中包含了客户端传进来的包裹。服务端拿到包裹后,会对包裹进行拆解,然后执行指定的服务函数,执行完毕后,再把执行结果放入客户端提供的reply包裹中。然后服务端向Binder驱动发送一个notify的消息,从而使得客户端线程从Binder驱动的代码区返回到客户端代码区。transact()的最后一个参数的含义是执行IPC调用的模式,分为两种:一种是双向,用常量0表示,其含义是服务端执行完指定的服务后返回一定的数据。另一种是单向,用常量1表示,其含义是不返回任何数据。最后,客户端就可以从reply中解析返回的数据了,同样,返回包裹中包含的数据也必须是有序的,而且这个顺序也必须是服务端和客户端事先约定好的。 + + 从这里可以看出,对应用程序开发员来说,客户端似乎是直接调用远程服务对应的Binder,而事实上则是通过Binder驱动进行了中转。即存在两个Binder对象,一个是服务端的Binder对象,另一个则是Binder驱动中的Binder对象,所不同的是Binder驱动中的对象不会再额外产生一个线程。 + + + +## Binder IPC原理 + + + +每个Android的进程,只能运行在自己进程所拥有的虚拟地址空间。对应一个4GB的虚拟地址空间,其中3GB是用户空间,1GB是内核空间,当然内核空间的大小是可以通过参数配置调整的。对于用户空间,不同进程之间彼此是不能共享的,而内核空间却是可共享的。Client进程向Server进程通信,恰恰是利用进程间可共享的内核内存空间来完成底层通信工作的,Client端与Server端进程往往采用ioctl等方法跟内核空间的驱动进行交互。 + + + +Binder通信采用C/S架构,从组件视角来说,包含Client、Server、ServiceManager以及binder驱动,其中ServiceManager用于管理系统中的各种服务。架构图如下所示: + +![ServiceManager](http://gityuan.com/images/binder/prepare/IPC-Binder.jpg) + +可以看出无论是注册服务和获取服务的过程都需要ServiceManager,需要注意的是此处的Service Manager是指Native层的ServiceManager(C++),并非指framework层的ServiceManager(Java)。ServiceManager是整个Binder通信机制的大管家,是Android进程间通信机制Binder的守护进程,要掌握Binder机制,首先需要了解系统是如何首次[启动Service Manager](http://gityuan.com/2015/11/07/binder-start-sm/)。当Service Manager启动之后,Client端和Server端通信时都需要先[获取Service Manager](http://gityuan.com/2015/11/08/binder-get-sm/)接口,才能开始通信服务。 + +图中Client/Server/ServiceManage之间的相互通信都是基于Binder机制。既然基于Binder机制通信,那么同样也是C/S架构,则图中的3大步骤都有相应的Client端与Server端。 + +1. **[注册服务(addService)](http://gityuan.com/2015/11/14/binder-add-service/)**:Server进程要先注册Service到ServiceManager。该过程:Server是客户端,ServiceManager是服务端。 +2. **[获取服务(getService)](http://gityuan.com/2015/11/15/binder-get-service/)**:Client进程使用某个Service前,须先向ServiceManager中获取相应的Service。该过程:Client是客户端,ServiceManager是服务端。 +3. **使用服务**:Client根据得到的Service信息建立与Service所在的Server进程通信的通路,然后就可以直接与Service交互。该过程:client是客户端,server是服务端。 + +图中的Client,Server,Service Manager之间交互都是虚线表示,是由于它们彼此之间不是直接交互的,而是都通过与[Binder驱动](http://gityuan.com/2015/11/01/binder-driver/)进行交互的,从而实现IPC通信方式。其中Binder驱动位于内核空间,Client,Server,Service Manager位于用户空间。Binder驱动和Service Manager可以看做是Android平台的基础架构,而Client和Server是Android的应用层,开发人员只需自定义实现client、Server端,借助Android的基本平台架构便可以直接进行IPC通信。 + + + + + + + +### 2.3 C/S模式 + +BpBinder(客户端)和BBinder(服务端)都是Android中Binder通信相关的代表,它们都从IBinder类中派生而来,关系图如下: + +![Binder关系图](http://gityuan.com/images/binder/prepare/Ibinder_classes.jpg) + +- client端:BpBinder.transact()来发送事务请求; +- server端:BBinder.onTransact()会接收到相应事务。 + + + + + +参考: https://www.zhihu.com/question/39440766/answer/93550572 + + + + + + +- [上一篇:7.嵌入式系统](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/7.%E5%B5%8C%E5%85%A5%E5%BC%8F%E7%B3%BB%E7%BB%9F.md) + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/OperatingSystem/AndroidKernal/2.Android\347\272\277\347\250\213\351\227\264\351\200\232\344\277\241\344\271\213Handler\346\266\210\346\201\257\346\234\272\345\210\266.md" "b/OperatingSystem/AndroidKernal/2.Android\347\272\277\347\250\213\351\227\264\351\200\232\344\277\241\344\271\213Handler\346\266\210\346\201\257\346\234\272\345\210\266.md" new file mode 100644 index 00000000..df78f0cf --- /dev/null +++ "b/OperatingSystem/AndroidKernal/2.Android\347\272\277\347\250\213\351\227\264\351\200\232\344\277\241\344\271\213Handler\346\266\210\346\201\257\346\234\272\345\210\266.md" @@ -0,0 +1,918 @@ +# 2.Android线程间通信之Handler消息机制 + + +Binder/Socket用于进程间通信,而Handler消息机制用于同进程的线程间通信,Handler消息机制是由一组MessageQueue、Message、Looper、Handler共同组成的,为了方便且称之为Handler消息机制。 + +有人可能会疑惑,为何Binder/Socket用于进程间通信,能否用于线程间通信呢?答案是肯定,对于两个具有独立地址空间的进程通信都可以,当然也能用于共享内存空间的两个线程间通信,这就好比杀鸡用牛刀。接着可能还有人会疑惑,那handler消息机制能否用于进程间通信?答案是不能,Handler只能用于共享内存地址空间的两个线程间通信,即同进程的两个线程间通信。很多时候,Handler是工作线程向UI主线程发送消息,即App应用中只有主线程能更新UI,其他工作线程往往是完成相应工作后,通过Handler告知主线程需要做出相应地UI更新操作,Handler分发相应的消息给UI主线程去完成,如下图: + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/handler_thread_commun.jpg) + +由于工作线程与主线程共享地址空间,即Handler实例对象mHandler位于线程间共享的内存堆上,工作线程与主线程都能直接使用该对象,只需要注意多线程的同步问题。工作线程通过mHandler向其成员变量MessageQueue中添加新Message,主线程一直处于loop()方法内,当收到新的Message时按照一定规则分发给相应的handleMessage()方法来处理。所以说,Handler消息机制用于同进程的线程间通信,其核心是线程间共享内存空间,而不同进程拥有不同的地址空间,也就不能用handler来实现进程间通信。 + + + +消息机制主要包含: + +- Message:消息分为硬件产生的消息(如按钮、触摸)和软件生成的消息;Message中有一个用于处理消息的Handler; +- MessageQueue:消息队列的主要功能向消息池投递消息(MessageQueue.enqueueMessage)和取走消息池的消息(MessageQueue.next);MessageQueue有一组待处理的Message; +- Handler:消息辅助类,主要功能向消息池发送各种消息事件(Handler.sendMessage)和处理相应消息事件(Handler.handleMessage);Handler中有Looper和MessageQueue。 +- Looper:不断循环执行(Looper.loop),按分发机制将消息分发给目标处理者。Looper有一个MessageQueue消息队列; + + +首先想一想平时我们是怎么使用的: +```java +private Handler mHandler = new Handler(new Handler.Callback() { + @Override + public boolean handleMessage(@NonNull Message msg) { + // xxxx. + return false; + } +}); + +mHandler.sendMessage(msg); +``` + +所以这里分析还是要从Handler开始,主要看它的构造函数和sendMessage()方法: + +## 1. Handler构造函数 +```java +final Looper mLooper; +final MessageQueue mQueue; +@UnsupportedAppUsage +final Callback mCallback; +final boolean mAsynchronous; +@UnsupportedAppUsage +IMessenger mMessenger; + +public Handler() { + this(null, false); +} + +public Handler(@Nullable Callback callback) { + this(callback, false); +} +// 可以设置Looper进来 +public Handler(@NonNull Looper looper) { + this(looper, null, false); +} + +public Handler(@Nullable Callback callback, boolean async) { + // 匿名类、内部类和本地类都必须申请为static,否则会警告可能出现内存泄露 + if (FIND_POTENTIAL_LEAKS) { + final Class klass = getClass(); + if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) && + (klass.getModifiers() & Modifier.STATIC) == 0) { + Log.w(TAG, "The following Handler class should be static or leaks might occur: " + + klass.getCanonicalName()); + } + } + // 1. 如果不设置Looper对象进来,Looper会通过Looper.myLooper()获取Looper对象 + mLooper = Looper.myLooper(); + if (mLooper == null) { + throw new RuntimeException( + "Can't create handler inside thread " + Thread.currentThread() + + " that has not called Looper.prepare()"); + } + // 获取Looper中的MessageQueue + mQueue = mLooper.mQueue; + // 回调接口 + mCallback = callback; + // 设置消息是否为异步处理方式,默认都是同步的 + // If true, the handler calls {@link Message#setAsynchronous(boolean)} for + // each {@link Message} that is sent to it or {@link Runnable} that is posted to it. + // 如果是异步的会调用Message的setAsynchronous方法,然后在MessageQueue中会判断是不是异步的消息 + mAsynchronous = async; +} + +``` + +### 上面看到会通过Looper类的myLooper()获取Looper对象 + +```java +// sThreadLocal.get() will return null unless you've called prepare(). +@UnsupportedAppUsage +static final ThreadLocal sThreadLocal = new ThreadLocal(); + +public static @Nullable Looper myLooper() { + return sThreadLocal.get(); +} + +/** + * Return the {@link MessageQueue} object associated with the current + * thread. This must be called from a thread running a Looper, or a + * NullPointerException will be thrown. + */ +public static @NonNull MessageQueue myQueue() { + return myLooper().mQueue; +} +``` +上面注释写的很明白:除非你调用了prepare()方法,不然sThreadLocal.get()会返回null。那sThreadLocal是啥? prepare()方法又是干啥的?这里还是分两步: + +#### sThreadLocal + +ThreadLocal:线程本地存储区(Thread Local Storage,简称为TLS),每个线程都有自己的私有的本地存储区域,不同线程之间彼此不能访问对方的TLS区域。TLS常用的操作方法: +- ThreadLocal.set(T value):将value存储到当前线程的TLS区域: + + ```java + public void set(T value) { + Thread t = Thread.currentThread(); + ThreadLocalMap map = getMap(t); + if (map != null) + map.set(this, value); + else + createMap(t, value); + } + ``` + +- ThreadLocal.get():获取当前线程TLS区域的数据: + + ```java + public T get() { + Thread t = Thread.currentThread(); + ThreadLocalMap map = getMap(t); + if (map != null) { + ThreadLocalMap.Entry e = map.getEntry(this); + if (e != null) { + @SuppressWarnings("unchecked") + T result = (T)e.value; + return result; + } + } + return setInitialValue(); + } + ``` + + +而sThreadLocal就是线程本地存储变量,它的意义就是在本线程内的任何对象内保持一致。 + + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/var_1.png) + + + +#### prepare()方法 + +```java +public static void prepare() { + prepare(true); +} + +private static void prepare(boolean quitAllowed) { + if (sThreadLocal.get() != null) { + throw new RuntimeException("Only one Looper may be created per thread"); + } + // 这个参数是是否允许退出 + sThreadLocal.set(new Looper(quitAllowed)); +} + +final MessageQueue mQueue; +final Thread mThread; +private Looper(boolean quitAllowed) { + // 创建MessageQueue,并将quitAllowed传入MessageQueue的构造函数 + mQueue = new MessageQueue(quitAllowed); + // 保存当前的线程 + mThread = Thread.currentThread(); +} +``` +从代码上可以看到prepare()方法的作用是创建一个Looper对象,然后将该Looper对象保存到sThreadLocal中。 + +这里有点麻烦了,我们上面使用的代码中并没有调用Looper.prepare()方法啊,理论上这里应该是null,Handler是无法使用的,为什么我们还能正常使用Handler?Looper.prepare()究竟是什么时候调用的?那我们需要看一下prepare()和prepare(boolean quitAllowed)方法都有哪些地方调用了: + +```java +/** + * Initialize the current thread as a looper, marking it as an + * application's main looper. The main looper for your application + * is created by the Android environment, so you should never need + * to call this function yourself. See also: {@link #prepare()} + */ +public static void prepareMainLooper() { + prepare(false); + synchronized (Looper.class) { + if (sMainLooper != null) { + throw new IllegalStateException("The main Looper has already been prepared."); + } + sMainLooper = myLooper(); + } +} +``` + +发现在Looper类中有个prepareMainLooper()的方法,注释上面写的也比较清楚,说这是Android运行环境创建的应用程序的主looper,你不能自己调用这个方法,那我们看一下这个方法是从哪里调用的,ActivityThread类中的main函数,看到这个main函数,就知道这个类不简单, + +```java +/** + * This manages the execution of the main thread in an + * application process, scheduling and executing activities, + * broadcasts, and other operations on it as the activity + * manager requests. + * + * {@hide} + */ +public final class ActivityThread extends ClientTransactionHandler { + // .... + public static void main(String[] args) { + Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain"); + + // Install selective syscall interception + AndroidOs.install(); + + // CloseGuard defaults to true and can be quite spammy. We + // disable it here, but selectively enable it later (via + // StrictMode) on debug builds, but using DropBox, not logs. + CloseGuard.setEnabled(false); + + Environment.initForCurrentUser(); + + // Make sure TrustedCertificateStore looks in the right place for CA certificates + final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId()); + TrustedCertificateStore.setDefaultUserDirectory(configDir); + + Process.setArgV0(""); + // 调用Looper.prepareMainLooper()方法 + Looper.prepareMainLooper(); + + // Find the value for {@link #PROC_START_SEQ_IDENT} if provided on the command line. + // It will be in the format "seq=114" + long startSeq = 0; + if (args != null) { + for (int i = args.length - 1; i >= 0; --i) { + if (args[i] != null && args[i].startsWith(PROC_START_SEQ_IDENT)) { + startSeq = Long.parseLong( + args[i].substring(PROC_START_SEQ_IDENT.length())); + } + } + } + ActivityThread thread = new ActivityThread(); + thread.attach(false, startSeq); + + if (sMainThreadHandler == null) { + sMainThreadHandler = thread.getHandler(); + } + + if (false) { + Looper.myLooper().setMessageLogging(new + LogPrinter(Log.DEBUG, "ActivityThread")); + } + + // End of event ActivityThreadMain. + Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); + // Looper.loop()启动 + Looper.loop(); + + throw new RuntimeException("Main thread loop unexpectedly exited"); + } +} +``` + +##### ActivityThread类 + +从上面ActivityThread类的注释中可以看到说ActivityThread是应用程序的主线程。ActivityThread类是Android APP进程的初始类,它的main函数是这个APP进程的入口。 + +[ActivityThread类的详细介绍可以参考这篇文章](),这里就不细说ActivityThread类了,我们只需要知道Android运行环境会启动ActivityThread类,他是主线程,而ActivityThread类中的main函数会调用Looper.prepareMainLooper()和Looper.loop()方法。 + + + +到这里梳理一下上面的TODO: + +1. Looper类构造函数创建MessageQueue +2. ActivityThread调用了Looper.prepareMainLooper()后又调用了Looper.loop()方法,要分析Looper.loop()方法的实现。 + + + +#### MessageQueue的实现 + +```java +private Looper(boolean quitAllowed) { + mQueue = new MessageQueue(quitAllowed); + mThread = Thread.currentThread(); +} + +public final class MessageQueue { + private final boolean mQuitAllowed; + private long mPtr; // used by native code + // Message中的next指向了下一个Message + Message mMessages; + private boolean mBlocked; + + MessageQueue(boolean quitAllowed) { + mQuitAllowed = quitAllowed; + mPtr = nativeInit(); + } + + private native static long nativeInit(); + private native static void nativeDestroy(long ptr); + @UnsupportedAppUsage + private native void nativePollOnce(long ptr, int timeoutMillis); /*non-static for callbacks*/ + private native static void nativeWake(long ptr); + private native static boolean nativeIsPolling(long ptr); + private native static void nativeSetFileDescriptorEvents(long ptr, int fd, int events); +} +``` + +MessageQueue中有一个native的方法; + +- nativeInit() + - 创建了NativeMessageQueue对象,增加其引用计数,并将NativeMessageQueue指针mPtr保存在Java层的MessageQueue + - 创建了Native Looper对象 + - 调用epoll的epoll_create()/epoll_ctl()来完成对mWakeEventFd和mRequests的可读事件监听 +- nativeDestroy()方法 + - 调用RefBase::decStrong()来减少对象的引用计数 + - 当引用计数为0时,则删除NativeMessageQueue对象 +- nativePollOnce + - 调用Looper::pollOnce()来完成,空闲时停留在epoll_wait()方法,用于等待事件发生或者超时 +- nativeWake + - 调用Looper::wake()来完成,向管道mWakeEventfd写入字符; + + + + + +#### Looper.loop()方法: + +```java +/** +* Run the message queue in this thread. Be sure to call +* {@link #quit()} to end the loop. +*/ +public static void loop() { + // 从sThreadLocal获取当前的Looper + final Looper me = myLooper(); + if (me == null) { + throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread."); + } + // 获取当前Looper的MessageQueue + final MessageQueue queue = me.mQueue; + + // 确保在权限检查时是基于本地进程,而不是调用进程 + // Make sure the identity of this thread is that of the local process, + // and keep track of what that identity token actually is. + Binder.clearCallingIdentity(); + final long ident = Binder.clearCallingIdentity(); + + // Allow overriding a threshold with a system prop. e.g. + // adb shell 'setprop log.looper.1000.main.slow 1 && stop && start' + final int thresholdOverride = + SystemProperties.getInt("log.looper." + + Process.myUid() + "." + + Thread.currentThread().getName() + + ".slow", 0); + + boolean slowDeliveryDetected = false; + // 不断去循环执行 + for (;;) { + // 去MessageQueue中的下一个Message,可能会堵塞 + Message msg = queue.next(); // might block + // 没有消息就退出该循环 + if (msg == null) { + // No message indicates that the message queue is quitting. + return; + } + + // This must be in a local variable, in case a UI event sets the logger + final Printer logging = me.mLogging; + if (logging != null) { + logging.println(">>>>> Dispatching to " + msg.target + " " + + msg.callback + ": " + msg.what); + } + + try { + // 分发Message,Message.target是Message中保存的当前handler对象,这里相对于调用Handler的dispatchMessage(msg)方法 + msg.target.dispatchMessage(msg); + if (observer != null) { + observer.messageDispatched(token, msg); + } + dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0; + } catch (Exception exception) { + if (observer != null) { + observer.dispatchingThrewException(token, msg, exception); + } + throw exception; + } finally { + ThreadLocalWorkSource.restore(origWorkSource); + if (traceTag != 0) { + Trace.traceEnd(traceTag); + } + } + // 恢复调用者信息 + // Make sure that during the course of dispatching the + // identity of the thread wasn't corrupted. + final long newIdent = Binder.clearCallingIdentity(); + // 将Message放入消息池 + msg.recycleUnchecked(); + } +} + +// Looper的quit方法是调用MessageQueue.quit()方法 +public void quit() { + mQueue.quit(false); +} + + +public void quitSafely() { + mQueue.quit(true); +} +``` + +loop()进入循环模式,不断重复下面的操作,直到没有消息时退出循环 + +- 读取MessageQueue的下一条Message; +- 把Message分发给相应的target; +- 再把分发后的Message回收到消息池,以便重复利用。 + +#### Message类 + +这里我们需要看一下Message类中的target及recycleUncheked()方法: + +```java +public final class Message implements Parcelable { + // 消息类别 + public int what; + // 参数1 + public int arg1; + // 参数2 + public int arg2; + // 消息内容 + public Object obj; + // 消息响应方Handler + /*package*/ Handler target; + // 维护下一个消息 + /*package*/ Message next; + // 消息的回调方法 + /*package*/ Runnable callback; + public static final Object sPoolSync = new Object(); + // sPool也是Message对象, + private static Message sPool; + private static int sPoolSize = 0; + + private static final int MAX_POOL_SIZE = 50; + + public static Message obtain(Handler h) { + Message m = obtain(); + // 将handler赋值给target保存 + m.target = h; + + return m; + } + + public void recycle() { + if (isInUse()) { + if (gCheckRecycle) { + throw new IllegalStateException("This message cannot be recycled because it " + + "is still in use."); + } + return; + } + recycleUnchecked(); + } + + void recycleUnchecked() { + // Mark the message as in use while it remains in the recycled object pool. + // Clear out all other details. + flags = FLAG_IN_USE; + what = 0; + arg1 = 0; + arg2 = 0; + obj = null; + replyTo = null; + sendingUid = UID_NONE; + workSourceUid = UID_NONE; + when = 0; + target = null; + callback = null; + data = null; + + synchronized (sPoolSync) { + // 如果当前消息池小于最大的数量限制,就把消息放到消息池中 + if (sPoolSize < MAX_POOL_SIZE) { + next = sPool; + // 把新放入的Message加到链表的表头 + sPool = this; + sPoolSize++; + } + } + } +} +``` + + + +## 2. Handler.sendMessage(Message)和dispatchMessage(Message)方法 + +```java +public void handleMessage(@NonNull Message msg) { +} + +/** + * Handle system messages here. + */ +public void dispatchMessage(@NonNull Message msg) { + if (msg.callback != null) { + // 如果Message存在回调方法,就回调Message.callback.run()方法 + handleCallback(msg); + } else { + if (mCallback != null) { + // 如果Message没有回调方法,而Handler对象中设置了Callback接口,这里就回调Callback.handleMessage(msg)方法 + if (mCallback.handleMessage(msg)) { + return; + } + } + // handler自己的handleMessage方法,默认空实现,子类可重写该方法 + handleMessage(msg); + } +} + +public final boolean sendEmptyMessageDelayed(int what, long delayMillis) { + Message msg = Message.obtain(); + msg.what = what; + return sendMessageDelayed(msg, delayMillis); +} + +private static Message getPostMessage(Runnable r) { + Message m = Message.obtain(); + m.callback = r; + return m; +} + +public final void removeCallbacksAndMessages(@Nullable Object token) { + mQueue.removeCallbacksAndMessages(this, token); +} +``` + + + +## MessageQueue类的常用方法 + +MessageQueue非常重要,因为quit、next等都最终调用的是MessageQueue类。 + + + +```java +public final class MessageQueue { + private final boolean mQuitAllowed; + private long mPtr; // used by native code + // 消息在MessageQueue中使用Message表示,而Message包含一个next变量,该变量指向下一个消息 + // 所以队列中的消息以链表的结构进行保存 + Message mMessages; + + // 获取MessageQueue的下一个Message + Message next() { + // 如果MessageQueue已退出就直接返回 + // Return here if the message loop has already quit and been disposed. + // This can happen if the application tries to restart a looper after quit + // which is not supported. + final long ptr = mPtr; + if (ptr == 0) { + return null; + } + + int pendingIdleHandlerCount = -1; // -1 only during first iteration + int nextPollTimeoutMillis = 0; + for (;;) { + if (nextPollTimeoutMillis != 0) { + Binder.flushPendingCommands(); + } + // 该方法的作用是从消息队列中取出一个消息。MessageQueue中没有保存消息队列,真正的消息队列在JNI的C代码中 + // 也就是在C环境中创建了一个NativeMessageQueue数据对象。该方法的第一个参数是int型变量,在C环境中该变量 + // 会被强制转换为一个NativeMessageQueue对象。如果消息队列中没消息,当前线程会被挂起。 + // 堵塞操作,当等待nextPollTimeoutMillis时长或消息队列别唤醒,都会返回 + // nextPollTimeoutMillis代表下一个消息到来前,还需要等待的时长;当nextPollTimeoutMillis = -1时,表示消息队列中无消息,会一直等待下去。 + nativePollOnce(ptr, nextPollTimeoutMillis); + + synchronized (this) { + // Try to retrieve the next message. Return if found. + final long now = SystemClock.uptimeMillis(); + Message prevMsg = null; + Message msg = mMessages; + + if (msg != null && msg.target == null) { + // 当当前的消息的Handler为空时,就查询异步消息 + // Stalled by a barrier. Find the next asynchronous message in the queue. + do { + prevMsg = msg; + msg = msg.next; + } while (msg != null && !msg.isAsynchronous()); + } + if (msg != null) { + // 仅仅是为了判断消息所指定的执行时间是否到了,如果到了就返回该消息 + if (now < msg.when) { + // Next message is not ready. Set a timeout to wake up when it is ready. + nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE); + } else { + // Got a message. + mBlocked = false; + if (prevMsg != null) { + prevMsg.next = msg.next; + } else { + mMessages = msg.next; + } + msg.next = null; + if (DEBUG) Log.v(TAG, "Returning message: " + msg); + // 设置消息的使用状态,即FLOAG_IN_USE的flag + msg.markInUse(); + return msg; + } + } else { + // No more messages. + nextPollTimeoutMillis = -1; + } + + // Process the quit message now that all pending messages have been handled. + if (mQuitting) { + dispose(); + return null; + } + + // If first time idle, then get the number of idlers to run. + // Idle handles only run if the queue is empty or if the first message + // in the queue (possibly a barrier) is due to be handled in the future. + if (pendingIdleHandlerCount < 0 + && (mMessages == null || now < mMessages.when)) { + pendingIdleHandlerCount = mIdleHandlers.size(); + } + if (pendingIdleHandlerCount <= 0) { + // No idle handlers to run. Loop and wait some more. + mBlocked = true; + continue; + } + + if (mPendingIdleHandlers == null) { + mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)]; + } + mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers); + } + // 空闲回调函数 + // Run the idle handlers. + // We only ever reach this code block during the first iteration. + for (int i = 0; i < pendingIdleHandlerCount; i++) { + final IdleHandler idler = mPendingIdleHandlers[i]; + mPendingIdleHandlers[i] = null; // release the reference to the handler + + boolean keep = false; + try { + keep = idler.queueIdle(); + } catch (Throwable t) { + Log.wtf(TAG, "IdleHandler threw exception", t); + } + + if (!keep) { + synchronized (this) { + mIdleHandlers.remove(idler); + } + } + } + + // Reset the idle handler count to 0 so we do not run them again. + pendingIdleHandlerCount = 0; + + // While calling an idle handler, a new message could have been delivered + // so go back and look again for a pending message without waiting. + nextPollTimeoutMillis = 0; + } + } + + // 添加一条消息到消息队列 + boolean enqueueMessage(Message msg, long when) { + // 每条消息必须有一个target + if (msg.target == null) { + throw new IllegalArgumentException("Message must have a target."); + } + if (msg.isInUse()) { + throw new IllegalStateException(msg + " This message is already in use."); + } + + synchronized (this) { + if (mQuitting) { + IllegalStateException e = new IllegalStateException( + msg.target + " sending message to a Handler on a dead thread"); + Log.w(TAG, e.getMessage(), e); + msg.recycle(); + return false; + } + + msg.markInUse(); + msg.when = when; + Message p = mMessages; + boolean needWake; + if (p == null || when == 0 || when < p.when) { + // New head, wake up the event queue if blocked. + msg.next = p; + mMessages = msg; + // 如果是堵塞的,这里就需要唤醒 + needWake = mBlocked; + } else { + // Inserted within the middle of the queue. Usually we don't have to wake + // up the event queue unless there is a barrier at the head of the queue + // and the message is the earliest asynchronous message in the queue. + needWake = mBlocked && p.target == null && msg.isAsynchronous(); + Message prev; + for (;;) { + prev = p; + p = p.next; + if (p == null || when < p.when) { + break; + } + if (needWake && p.isAsynchronous()) { + needWake = false; + } + } + msg.next = p; // invariant: p == prev.next + prev.next = msg; + } + + // We can assume mPtr != 0 because mQuitting is false. + if (needWake) { + // 内部会将mMessages消息添加到C环境中的消息队列中,并且如果消息线程正处于挂起状态,则唤醒该线程 + nativeWake(mPtr); + } + } + return true; + } + + + + void quit(boolean safe) { + if (!mQuitAllowed) { + throw new IllegalStateException("Main thread not allowed to quit."); + } + + synchronized (this) { + if (mQuitting) { + return; + } + mQuitting = true; + + if (safe) { + removeAllFutureMessagesLocked(); + } else { + removeAllMessagesLocked(); + } + + // We can assume mPtr != 0 because mQuitting was previously false. + nativeWake(mPtr); + } + } + + // 清除所有的message + private void removeAllMessagesLocked() { + Message p = mMessages; + while (p != null) { + Message n = p.next; + p.recycleUnchecked(); + p = n; + } + mMessages = null; + } + + void removeCallbacksAndMessages(Handler h, Object object) { + if (h == null) { + return; + } + + synchronized (this) { + Message p = mMessages; + // 从消息队列的头开始,移除所有符合条件的消息 + // Remove all messages at front. + while (p != null && p.target == h + && (object == null || p.obj == object)) { + Message n = p.next; + mMessages = n; + p.recycleUnchecked(); + p = n; + } + // 移除剩余符合条件的消息 + // Remove all messages after front. + while (p != null) { + Message n = p.next; + if (n != null) { + if (n.target == h && (object == null || n.obj == object)) { + Message nn = n.next; + n.recycleUnchecked(); + p.next = nn; + continue; + } + } + p = n; + } + } + } +``` + + + + + + + +# Handler内存泄露 + + + +有关内存泄露请猛戳[内存泄露][1] + +在上面分析Handler类源码时,其构造函数中第一部分的代码就是 匿名类、内部类和本地类都必须申请为static,否则会警告可能出现内存泄露。那为什么会导致内存泄露呢? + +```java +Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + // do something. + } +} +``` +当我们这样创建`Handler`的时候`Android Lint`会提示我们这样一个`warning: In Android, Handler classes should be static or leaks might occur.`。 + +一直以来没有仔细的去分析泄露的原因,先把主要原因列一下: +- `Android`程序第一次创建的时候,默认会创建一个`Looper`对象,`Looper`去处理`Message Queue`中的每个`Message`,主线程的`Looper`存在整个应用程序的生命周期. +- `Hanlder`在主线程创建时会关联到`Looper`的`Message Queue`,`Message`添加到消息队列中的时候`Message(排队的Message)`会持有当前`Handler`引用, +当`Looper`处理到当前消息的时候,会调用`Handler#handleMessage(Message)`.就是说在`Looper`处理这个`Message`之前, +会有一条链`MessageQueue -> Message -> Handler -> Activity`,由于它的引用导致你的`Activity`被持有引用而无法被回收 +- **在java中,no-static的内部类会隐式的持有当前类的一个引用。static的内部类则没有。** + +## 具体分析 +```java +public class SampleActivity extends Activity { + + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + // do something + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // 发送一个10分钟后执行的一个消息 + mHandler.postDelayed(new Runnable() { + @Override + public void run() { } + }, 600000); + + // 结束当前的Activity + finish(); +} +``` +在`finish()`的时候,该`Message`还没有被处理,`Message`持有`Handler`,`Handler`持有`Activity`,这样会导致该`Activity`不会被回收,就发生了内存泄露. + +## 解决方法 +- 通过程序逻辑来进行保护。 + - 如果`Handler`中执行的是耗时的操作,在关闭`Activity`的时候停掉你的后台线程。线程停掉了,就相当于切断了`Handler`和外部连接的线, + `Activity`自然会在合适的时候被回收。 + - 如果`Handler`是被`delay`的`Message`持有了引用,那么在`Activity`的`onDestroy()`方法要调用`Handler`的`remove*`方法,把消息对象从消息队列移除就行了。 + - 关于`Handler.remove*`方法 + - `removeCallbacks(Runnable r)` ——清除r匹配上的Message。 + - `removeC4allbacks(Runnable r, Object token)` ——清除r匹配且匹配token(Message.obj)的Message,token为空时,只匹配r。 + - `removeCallbacksAndMessages(Object token)` ——清除token匹配上的Message。 + - `removeMessages(int what)` ——按what来匹配 + - `removeMessages(int what, Object object)` ——按what来匹配 + 我们更多需要的是清除以该`Handler`为`target`的所有`Message(Callback)`就调用如下方法即可`handler.removeCallbacksAndMessages(null)`; +- 将`Handler`声明为静态类。 + 静态类不持有外部类的对象,所以你的`Activity`可以随意被回收。但是不持有`Activity`的引用,如何去操作`Activity`中的一些对象? 这里要用到弱引用 + +```java +public class MyActivity extends Activity { + private MyHandler mHandler; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mHandler = new MyHandler(this); + } + + @Override + protected void onDestroy() { + // Remove all Runnable and Message. + mHandler.removeCallbacksAndMessages(null); + super.onDestroy(); + } + + static class MyHandler extends Handler { + // WeakReference to the outer class's instance. + private WeakReference mOuter; + + public MyHandler(MyActivity activity) { + mOuter = new WeakReference(activity); + } + + @Override + public void handleMessage(Message msg) { + MyActivity outer = mOuter.get(); + if (outer != null) { + // Do something with outer as your wish. + } + } + } +} +``` + +[1]:(https://github.com/CharonChui/AndroidNote/blob/master/BasicKnowledge/%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F.md) + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/OperatingSystem/AndroidKernal/3.Android Framework\346\241\206\346\236\266.md" "b/OperatingSystem/AndroidKernal/3.Android Framework\346\241\206\346\236\266.md" new file mode 100644 index 00000000..100b61d7 --- /dev/null +++ "b/OperatingSystem/AndroidKernal/3.Android Framework\346\241\206\346\236\266.md" @@ -0,0 +1,105 @@ +# 3.Android Framework框架 + +## Framework框架 + +Framework定义了客户端组件和服务端组件功能及接口。后面的阐述中,”应用程序“一般是指”.apk“程序。 + +框架中包含三个主要部分,分别是服务端、客户端和Linux驱动。 + + + +### 服务端 + +服务端主要包含两个重要类,分别是WindowManagerService(WmS)和ActivityManagerService(AmS)。 + +WmS的作用是为所有应用程序分配窗口,并管理这些窗口。包括分配窗口的大小,调节各窗口的叠放次序,隐藏或显示窗口。 + +AmS的作用是管理所有应用程序中的Activity。 + +除此之外,在服务端还包括两个消息处理类: + +- KeyQ类:该类为WmS的内部类,继承与KeyInputQueue类,KeyQ对象一旦创建,就立即启动一个线程,该线程会不断地读取用户的UI操作信息,比如按键、触摸屏、trackball、鼠标等,并把这些消息放到一个消息队列QueueEvent类中。 +- InputDispatcherThread类:该类的对象一旦创建,也会立即启动一个线程,该线程会不断地从QueueEvent中取出用户消息,并进行一定的过滤,过滤后,再将这些消息发送给当前活动的客户端程序中。 + + + +### 客户端 + + + +客户端主要包括以下重要类: + +- ActivityThread类:该类为应用程序的主线程类,所有的APK程序都有且仅有一个ActivityThread类,程序的入口为该类中的static main()函数。 +- Activity类:该类为APK程序中的一个最小运行单元,一个APK程序中可以包含多个Activity对象,ActivityThread类会根据用户操作选择运行哪个Activity对象。 +- PhoneWindow类:该类继承于Window类,同时,PhoneWindow类内部包含了一个DecorView对象。简而言之,PhoneWindow是把一个FrameLayout进行了一定的包装,并提供了一组通用的窗口操作接口。 +- Window类:该类提供了一组通用的窗口(Window)操作API,这里的窗口仅仅是程序层面上的,WmS所管理的窗口并不是Window类,而是一个View或者ViewGroup类,一般就是指DecorView类,即一个DecorView就是WmS所管理的一个窗口。Window是一个abstract类型。 +- DecorView类:该类是一个FrameLayout的子类,并且是PhoneWindow中的一个内部类。Decor的英文是Decoration,即”装饰“的意思,DecorView就是对普通的FrameLayout进行了一定的装饰,比如添加一个通用的Titlebar,并相应特定的按键消息等。 +- ViewRoot类:WmS管理客户端窗口时,需要通知客户端进行某种操作,这些都是通过异步消息完成的,实现的方式就是使用Handler,ViewRoot类就是继承于Handler,其作用主要是接收WmS的通知。 +- W类:该类继承于Binder,并且是ViewRoot的一个内部类。 +- WindowManager类:客户端要申请创建一个窗口,而具体创建窗口的任务是由WmS完成的,WindowManager类就像是一个部门经理,谁有什么需求就告诉它,由它和WmS进行交互,客户端不能直接和WmS进行交互。 + + + +### Linux驱动 + +Linux驱动和Framework相关的主要包含两部分: + +- SurfaceFlingger(SF)驱动:每一个窗口都对应一个Surface,SF驱动的作用是把各个Surface显示在同一个屏幕上。 +- Binder驱动:Binder驱动的作用是提供进程间的消息传递。 + + + +## APK程序的运行过程 + +1. 首先,ActivityThread从main()函数开始执行,调用prepareMainLooper()为UI线程创建一个消息队列(MessageQueue)。 +2. 创建一个ActivityThread对象,在ActivityThread的初始化代码中会创建一个H(Handler)对象和一个ApplicationThread(Binder)对象。其中Binder负责接收远程AmS的IPC调用,接收到调用后,则通过Handler把消息发送到消息队列,UI主线程会异步地从消息队列中取出消息并执行相应的操作,比如start、stop、pause等。 +3. UI主线程调用Looper.loop()方法进入消息循环体,进入后就会不断地从消息队列中读取并处理消息。 +4. 当ActivityThread接收到AmS发送发送start某个Activity后,就会创建指定的Activity对象。Activity又会创建PhoneWindow类 ==》 DecorView类 ==》 创建相应的View或ViewGroup。创建完成后,Activity需要把创建好的界面显示到屏幕上,于是调用WindowManager类,后者于是创建一个ViewRoot对象,该对象实际上创建了ViewRoot类和W类,创建ViewRoot对象后,WindowManager再调用WmS提供的远程接口完成添加一个窗口并显示到屏幕上。 +5. 接下来,用户开始在程序界面上操作。KeyQ线程不断把用户消息存储到QueueEvent队列中,InputDispatch而Thread线程逐个去除消息,然后调用WmS中的相应函数处理该消息。当WmS发现该消息属于客户端某个窗口时,就会调用相应窗口的W接口。W类是一个Binder,负责接收WmS的IPC调用,并把调用消息传递给ViewRoot,ViewRoot再把消息传递给UI主线程ActivityThread,ActivityThread解析该消息并作出相应的处理。在客户端程序中,首先处理消息的是DecorView,如果DecorView不想处理某个消息,则可以将该消息传递给其内部包含的子View或者ViewGroup,如果还没有处理,则传递给PhoneWindow,最后再传递给Activity。 + + + +上面启动完后会有几个线程? 每个Binder对象都对应一个线程,Activity启动后会创建一个ViewRoot.W对象,同时ActivityThread会创建一个ApplicationThread对象,这两个对象都继承于Binder,因此会启动两个线程,负责接收Binder驱动发送IPC调用。最后一个主要线程也就是程序本身所在的线程,也叫做用户交互(UI)线程,因为所有的处理用户消息,以及绘制界面的工作都在该线程中完成。 + + + +## 窗口相关概念 + +窗口、Window类、ViewRoot类以及W类的区别和联系: + +- 窗口(Window):这是一个纯语义的说法,即程序员所看到的屏幕上的某个独立的界面,比如一个带有TitleBar的Activity界面、一个对话框、一个Menu菜单等,这些都称之为窗口。从WmS的角度来讲,窗口是接收用户消息的最小单元,WmS内部用特定的类表示一个窗口,以实现对窗口的管理。WmS接收到用户消息后,首先要判断这个消息属于哪个窗口,然后通过IPC调用把这个消息传递给客户端的ViewRoot类。 +- Window类:该类在android.view包中,是一个abstract类,该类是对包含有可视界面的窗口的一种包装,所谓的可视界面就是指各种View或者ViewGroup,一般可以通过res/layout目录下的xml文件描述。 +- ViewRoot类:该类是android.view包中,客户端申请创建窗口时需要一个客户端代理,用以和WmS进行交互,这个就是ViewRoot的功能,每个客户端的窗口都会对应一个ViewRoot类。 +- W类:该类是ViewRoot类的一个内部类,继承于Binder,用于想WmS提供一个IPC接口,从而让WmS控制窗口客户端的行为。 + +描述一个窗口之所以有这么多类的原因在于,窗口的概念存在于客户端和服务端(WmS)之中,客户端所理解的窗口和服务端理解的窗口是不同的,因此,在客户端和服务端会用不同的类来描述窗口。比如在客户端,用户能看到的窗口一般是View或者ViewGroup组成的窗口,而与Activity对应的窗口却是一个DecorView类,而具备常规Phone操作的接口却又是一个PhoneWindow类。所以无论是在客户端还是服务端,对窗口都有不同层面的抽象。 + + + +## Context + + + +Context在应用程序开发中会经常被使用,在一般的计算机书记中,Context被翻译为“上下文”,但是在Android中感觉翻译为“场景”更容易理解一些。 + + + +一个Context意味着一个场景,一个场景就是用户和操作系统交互的一种过程。比如当你打电话时,场景包括电话程序对应的界面以及隐藏在界面后的数据。当你看短信时,场景包括短信界面以及隐藏在后面的数据。这也就是为什么一个Activity就是一个Context,一个Service也是一个Context。因为Android程序员把“场景”抽象为Context类,他们认为用户和操作系统的每一次交互都是一个场景,比如打电话、发短信,这些都是有界面的场景,还有一些没有界面的场景,比如后台运行的服务(Service)。一个应用程序可以认为是一个工作环境,用户在这个工作环境汇总会切换到不同的场景,这就像一个前台秘书,她可能需要接待客人,可能要打印文件,还可能要接听客户电话,而这些就称之为不同的场景,前台秘书可称之为一个应用程序。 + + + +### Context的创建 + +在创建Application、Activity、Service时,AmS通过远程调用到ActivityThread的bindApplication()方法或ActivityThread的scheduleLaunchActivity()或ActivityThread.scheduleCreateService()方法,这里面会去创建ContextImpl并初始化。 + + + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/OperatingSystem/AndroidKernal/4.ActivityManagerService\347\256\200\344\273\213.md" "b/OperatingSystem/AndroidKernal/4.ActivityManagerService\347\256\200\344\273\213.md" new file mode 100644 index 00000000..0a5b9007 --- /dev/null +++ "b/OperatingSystem/AndroidKernal/4.ActivityManagerService\347\256\200\344\273\213.md" @@ -0,0 +1,148 @@ +# 4.ActivityManagerService简介 + +ActivityManagerService.java文件,简称AmS,是Android内核的三大核心功能之一,另外两个是WindowManagerService.java和View.java。Activity的管理实际上由三个主要类完成,分别是AmS、ActivityRecord及ActivityTask。AmS内部用ActivityRecord来表示一个Activity对象。ActivityStack是一个描述Task的类,所有和Task相关的数据以及控制都由ActivityStack来实现。 + +AmS所提供的主要功能包括以下几项: + +- 统一调度各应用程序的Activity。应用程序要运行Activity,会首先报告给AmS,然后由AmS决定该Activity是否可以启动,如果可以,AmS再通知应用程序运行指定的Activity。换句话说,运行Activity是各应用程序的内政,AmS并不干预,但是AmS必须知道应用进程都运行了哪些Activity。 +- 内存管理。Android官方声称,Activity退出后,其所在的进程并不会被立即杀死,从而下次在启动该Activity时能够提高启动速度。这些Activity只有当系统内存紧张时,才会被自动杀死,应用程序不用关心这个问题。而这些正是AmS中完成的。 +- 进程管理。AmS向外提供了查询系统正在运行的进程信息的API。 + + + +## Activity调度机制 + + + +在Android中,Activity调度的基本思路是:各应用进程要启动新的Activity或者停止当前的Activity,都要首先报告给AmS,而不能“擅自处理”。AmS在内部为所有应用程序都做了记录,当AmS接到启动或停止的报告时,首先更新内部记录,然后再通知相应客户进程运行或者停止指定的Activity。由于AmS内部有所有Activity的记录,也就理所当然地能够调度这些Activity,并根据Activity和系统内存的状态自动杀死后台的Activity。 + +具体的讲,启动一个Activity有以下集中方式: + +- 在应用程序中调用startActivity()启动指定的Activity。 +- 在Home程序中单击一个应用图标,启动新的Activity。 +- 按Back键,结束当前Activity,自动启动上一个Activity。 +- 长按Home键,显示出当前任务列表,从中选择一个启动。 + +这四种启动方式的主体处理流程都会按照第一种启动方式运行,后三种方式只是在前端消息处理上各有不同。 + +AmS中定义了几个重要的数据类,分别用来保存进程(Process)、活动(Activity)和任务(Task)。 + +### 进程数据类ProcessRecord + +ProcessRecord记录一个进程中的相关信息,该类中内部变量可以分为三个部分: + +- 进程文件信息:也就是与该进程对应的APK文件的内部信息,例如ApplicationInfo、processName等。 +- 该进程的内存状态信息:这些信息将用于Linux系统的Out Of Memory情况的处理,当发生系统内部不够用时,Linux系统会根据进程的内存状态信息,杀掉优先级比较低的进程。 +- 进程中包含的Activity、Provider、Service等:如ArrayList activities; + +### HistoryRecord数据类 + +AmS中使用HistoryRecord数据类来保存每个Activity的信息,这里非常奇怪,Activity本身也是一个类,为什么还要用HistoryRecord来保存Activity的信息,而不是直接用Activity呢?这是因为Activity是具体的功能类,这就好比每一个读者都是一个Activity,而学校要为每一个读者建立一个档案,这些档案中并不包含每个读者具体的学习能力,而只是学生的籍贯信息、姓名、出生日期等,HistoryRecord就是AmS为每一个Activity建立的档案,该数据类中的变量主要包含两部分: + +- 环境信息:该Activity的工作环境,比如隶属于哪个Package,所在的进程名称、文件路径、数据路径、图标主题等。 +- 运行状态信息:比如idle、stop、finishing等,这些变量一般问boolean类型,这些状态值与应用程序中的onCreate、onPause、onStart等状态有所不同。 + +HistoryRecord类也是一个Binder,它基于IApplication.Stub类,因此它可以被IPC调用,一般是在WmS中进行该对象的IPC调用。 + + + +### TaskRecord类 + +AmS中使用任务的概念确保Activity启动和退出的顺序。比如如下启动流程,A、B、C分别代表三个应用程序,数字1、2、3分别代表该应用中的Activity。 + +A1 -> A2 -> A3 -> B1 -> B2 -> C1 -C2,此时应该处于C2,如果AmS中没有任务的概念,此时又要从C2启动B1,那么会存在以下两个问题: + +- 虽然程序上是要启动B1,但是用户可能期望启动B2,因为B1和B2是两个关联的Activity,并且B2已经运行于B1之后。如何提供给程序员一种选择,虽然指定启动B1,但如果B2已经运行,那么就启动B2. +- 假设已经成功从C2跳转到B2,此时如果用户按Back键,是应该回到B1呢,还是应该回到C2? + +任务概念的引入正是为了解决以上两个问题,HistoryRecord中包含一个init task变量,保存该Activity所属哪个任务,程序员可以使用Intent.FLAG_NEW_TASK标识告诉AmS为启动的Activity重新创建一个Task。 + +有了Task的概念后,以上情况就会是这样: + +虽然程序明确指定从C2启动到B1,程序员可以在intent的FLAG中添加NEW_TASK标识,从而使得AmS会判断B1是否已经在mHistory中。如果在,则找到B1所在的Task,并从该Task中的最上面的Activity出运行,此处也就是B2.当然,如果程序的确要启动B1,那么就不要使用NEW_TASK标识,使用的话,mHistory中会有两个B1记录,隶属于不同的Task。 + +TaskRecord类内部变量如下: + +- taskId:每一个任务对应一个Int型的标识 +- intent:创建该任务对应的intent +- numActivities:该任务中的Activity数目 + +TaskRecord中并没有该任务中所包含的Activity的列表,比如ArrayList或者HistoryRecord[]之类的变量,这意味着不能直接通过任务id找到其所包含的Activity。 + + + +### AmS中调度相关常量 + +- MAX_ACTIVITIES = 20; + + 系统只能有一个Activity处于执行状态,对于非执行状态的Activity,AmS会在内部暂时缓存起来,而不是立即杀死,但如果后台的Activity超过该常量,则会强制杀死一些优先级较低的Activity。 + +- PAUSE_TIMEOUT = 500; + + 当AmS通知应用程序暂停指定的Activity时,AmS的忍耐是有限的,因为只有500毫秒,如果应用程序在该常量时间内还没有暂停,AmS会强制暂停并关闭该Activity。这就是为什么不能在onPause()中做过多事情的原因。 + +- LAUNCH_TIMEOUT = 10 * 1000:当AmS通知应用程序启动某个Activity时,如果超过10s,AmS就会放弃 + +- PROC_START_TIMEEOUT = 10 * 1000:当AmS启动某个客户进程后,客户进程必须在10秒之内报告AmS自己已经启动,否则AmS会认为指定的客户进程不存在。 + +### 等待序列 + +由于AmS采用Service机制运作,所有的客户进程要做什么事情,都要先请求AmS,因此,AmS内部必须有一些消息序列保存这些请求,并按顺序依次进行相应的操作: + +- final ArrayList mHistory = new ArrayList(); + + 这是最最重要的内部变量,该变量保存了所有正在运行的Activity,所谓正在运行是指该HistoryRecord的finishing状态为true。比如当前和用户交互的Activity属于正在运行,从A1启动到A2,尽管A1看不见了,但是仍然是正在运行。从A2按Home键回到桌面,A2也是正在运行,但如果从A2按Back键回到A1,这时A2就不是正在运行状态了,它会从mHistory中删除掉 。 + +- private final ArrayList mLRUActivities = new ArrayList(); + + LRU代表Latest Recent Used,即最近所用的Activity的列表,它不像mHistory仅保存正在运行的Activity,mLRUActirity会保存所有过去启动过的Activity。 + +- final ArrayList mPendingActivityLaunches = new ArrayList(); + + 当AmS内部还没有准备好时,如果客户进程请求启动某个Activity,那么会被暂时保存到该变量中,这也就是pending的含义。这中情况一般发生在系统启动时,系统进程会查询系统中所有属性为Persisitant的客户进程,此时由于AmS也正在启动,因此,会暂时保存这些请求。 + +- final ArrayList mStoppingActivitiies; + + 在AmS的设计中,有这样一个理念:优先启动,其次再停止。即当用户请求启动A2时,如果A1正在运行,AmS首先会暂停A1,然后启动A2.当A2启动后再停止A1.在这个过程中,A1会被临时保存到mStoppingActivities中,直到A2启动后并处于空闲时,再回过头来停止mStoppingActivities中保存的HistoryRecord列表。 + +- final ArrayList mFinishingActivities; + + 和mStoppingActivities类似,当AmS认为某个Activity已经处于finish状态时,不会立即杀死该Activity,而是会保存到该变量中,直到超过系统设定的警戒线后,才去回收该变量中的Activity。 + +- HistoryRecord mPausingActivity + + 正在暂停的Activity,该变量只有在暂停某个Activity时才有值,代表正在暂停的Activity + +- HistoryRecord mResumedActivity + + 当前正在运行的Activity,这里的正在运行不一定是正在与用户交互。比如当用户请求执行A2时,当前正在运行的A1,此时AmS会首先暂停A1,而在暂停的过程中,AmS会通知WmS暂停获取用户消息,而此时mResumedActivity依然是A1. + +- HistoryRecord mFocusedActivity + + 这里的Focus并非是正在和用户交互,而是AmS通知WmS应该和用户交互的Activity,而在WmS真正处理这个消息之前,用户还是不能和该Activity交互。 + +- HistoryRecord mLastPausedActivity + + 上一次暂停的Activity + + + +当系统内存低时,AmS会要求客户端释放内存,而可能会释放Surface对应的内存,而这是由WmS具体完成的。当WmS释放了Surface内存后,该Surface对应的窗口就无效了,则该窗口对应的Activity也就无效了,则Activity所在的进程也就无效了。 + + + +## startActivi()启动流程 + + + +- 调用startActivity()方法后,通知到AmS,AmS收到请求startActivity()后,会首先暂停当前的Activity,因此这时候AmS需要判断mResumeActivity是否为空,也就是当前有没有正在运行的activity。一般情况下,该值都不会为空。 +- 如果为空的话继续往下走,如果不为空的话,AmS会通知当前mResumeActivity对应的Activity所在的进程暂停,然后AmS就不管了,当那个进程暂停完后会报告AmS,这时AmS开始执行completePaused()。该方法中会去检查要启动的目标Activity是否存在mHistory列表中,如果存在说明目标进程还在运行,只是目标Activity处于stop状态,还没有finish,所以会通知B进程则通过handleResumeActivity()方法来resume目标Activity。 +- 如果不存在那需要去检查目标Activity所在的进程是否存在。如果不存在则必须首先启动对应的进程。这时AmS调用Process进程类启动一个新的进程,新的进程会从ActivityThread的main()函数处开始执行,当对应进程启动后,B进程会报告AmS自己已经启动,于是执行AmS的attachApplication()方法,该方法可理解为B进程请求AmS给自己安排(attach)一个具体要执行的Activity,此时AmS继续调用resumeTopActivity(),通知B进程执行指定的Activity。 +- 首先判断目标HistoryRecord在B进程中不存在,则B调用handleLaunchActivity()创建一个该Activity实例。如果已经存在,则调用handleResumeActivity()恢复已有的Activity运行。这个逻辑的意思就是说,在ActivityThread中可以存在同一个Activity的多个实例,对应了AmS中mHistory的多个HistoryRecord对象。在一般情况下,当调用startActivity的FLAG为NEW_TASK时,AmS会首先从mHIstory中找到指定Activity所在的Task,然后启动Task中的最后一个Activity。如果FLAG不为NEW_TASK,那么AmS会在当前Task中重新创建一个HistoryRecord。 + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/OperatingSystem/AndroidKernal/5.Android\346\266\210\346\201\257\350\216\267\345\217\226.md" "b/OperatingSystem/AndroidKernal/5.Android\346\266\210\346\201\257\350\216\267\345\217\226.md" new file mode 100644 index 00000000..f731b4e9 --- /dev/null +++ "b/OperatingSystem/AndroidKernal/5.Android\346\266\210\346\201\257\350\216\267\345\217\226.md" @@ -0,0 +1,60 @@ +# 5.Android消息获取 + +在Android 2.0及以前的所有版本中,获取用户消息的方式基本上都是相同的,具体如下: + +- 在WmS中有一个子类KeyQ,该类基于KeyInputQueue类,而该类内部则包含一个线程对象,即KeyQ对象是一个线程对象。该线程的任务是调用native方法从输入设备中读取用户消息,包括案件、触摸屏、鼠标、轨迹球等各种消息,并把读取到的消息保存到一个QueueEvent队列中。 + +- 在WmS中有另外一个子类叫做InputDispatcherThread,该类也是一个线程类,即内部包含一个线程。该线程的任务就是从上面的QueueEvent队列中读取用户消息,并对这些消息进行一定的加工,然后判断应该把这个消息发送给哪个应用窗口。 +- 在每一个应用窗口对象ViewRoot中都包含一个W子类,该类是一个Binder类,InputDispatchThread通过IPC方式调用W所提供的函数。从而把消息发送给对应的客户端窗口。 + +2.2版本中的这种处理过程有两点被Android社区所诟病。 + +- 对所有原始消息的加工都在KeyQ类的Java代码中完成,这加大了消息处理的延迟。 +- InputDispatcherThread是通过Binder方式传递按键消息的,而Binder的延迟降低了用户操作的相应速度。 + +以上两点给用户的体验就是界面操作的延迟,比如滑动触摸屏,界面的移动会延迟于指尖的移动。于是从2.3开始,Android团队对消息处理逻辑进行了重构: + +- 获取消息的代码全部使用C++完成,包括对消息进行加工转换。 +- 抛弃了使用Binder方式传递用户消息到客户端,而是使用Linux的Pipe机制。 + + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/input_message_process.png) + +1. 首先,InputReader线程会持续调用输入设备的驱动,读取所有用户输入的消息,该线程和InputDispatcher线程都在系统进程(system_process)空间中运行。InputDispatcher线程从自己的消息队列中取出原始消息,取出的消息有可能经过两种方式进行派发。 + - 经过管道(Pipe)直接派发到客户窗口中。 + - 先派发到WmS中,由WmS经过一定的处理,如果WmS没有处理该消息,则再派发给客户窗口中,否则,不派发到客户窗口。 +2. 应用程序添加窗口时,会在本地创建一个ViewRoot对象,然后通过IPC调用WmS中的Session对象的addWindow()方法,从而请求WmS创建一个窗口。WmS会把窗口的相关信息保存在内部的一个窗口列表类InputMonitore中,然后使用InputManager类把这些窗口信息传递给InputDispatcher线程。传递的过程中,InputManager类需要调用JNI代码,把这些窗口信息传递给NativeInputManager对象中。 +3. 当InputDispatcher得到用户消息后,会根据NativeInputManager中保存的所有窗口信息判断当前的活动窗口是哪个,并把消息传递给该活动窗口。另外,如果是按键消息,InputDispatcher会先回调InputManager中定义的回调函数,这既会回调InputMonitor中的回调函数,又会回调WmS中定义的相关函数,所以这些回调函数的返回值类型是boolean。对于系统按键消息,比如“Home键”、电话按键等,WmS内部会按默认的方式处理,并返回false,从而InputDispatcher不会继续把这些按键消息传递给客户窗口。对于触摸屏消息,InputDispatcher则直接传递给客户窗口。 +4. 在InputDispatcher和客户窗口之间使用了管道(Pipe)机制进行消息传递。Pipe是Linux的一种系统调用,Linux会在内核地址空间中开辟一段共享内存,并产生一个Pipe对象。每个Pipe对象内部都会自动创建两个文件描述符,一个用于读,另一个用于写。应用程序可以调用pipe()函数产生一个Pipe对象,并获得该对象中的读、写文件描述符。文件描述符是全局唯一的,从而使得两个进程之间可以借助这两个描述符,一个往管道中写数据,另一个从管道中读数据。管道只能是单向的,因此,如果两个进程要进行双向消息传递,必须创建两个管道。当客户窗口请求WmS创建窗口时,WmS内部会创建两个管道,其中一个管道用于InputDispatcher向客户窗口传递消息,另一个用户客户窗口向InputDispatcher报告消息的执行结果。因此,有多少个客户窗口,就有多少个管道与InputDispatcher相连。 +5. 由于创建管道属于Linux系统调用,Java不能直接调用,另外也由于程序结构的需要,Android中把调用管道的相关操作封装到了InputChannel.java类中,同时该类中保存了InputDispatcher和客户窗口的管道收、发描述符。因此,InputChannel也可以理解为一个“通道”,在InputDispatcher端存在一个服务端消息通道(serverChannel)。在客户窗口端存在一个客户端消息通道(clientChannel)。管道和通道的区别是,管道是Linux系统的概念,使用pipe()系统调用就可以创建一个管道,而通道是Android内部定义的一个概念,主要保存了通信双发所使用的管道描述符。 + + + +### 事件分发 + + + +#### 按键消息派发 + +1. 首先,在ViewRoot中定义了一个InputHandler对象,当底层得到按键消息后,会回调到该InputHandler对象的handleKey()函数,该函数内部发送一个异步DISPATCH_KEY消息,消息的处理函数为deliverKeyEvent(),该函数内部分以下三步执行: + - 调用mView.dispatchKeyEventPreime(),这里的PreIme的意思就是在Ime之前,即输入法之前。因为对于View系统来讲,如果有输入法窗口存在,会先将案件消息派发给输入法窗口,只有当输入法窗口没有处理该消息时,才会把消息继续派发给真正的视图。所以如果想在输入法之前处理某些按键消息,可以重写该dispatchKeyEventPreime()方法,如果该函数返回为true,则可以直接返回了,但是在返回之前如果WmS要求返回一个处理回执,则需要先调用finishInputEvent()报告给WmS已经处理的该消息,从而使得WmS可以继续派发下一个消息。 + - 接下来就需要把该消息派发到输入法窗口。当然如果此时输入法窗口不存在,就直接派发到真正的视图。 + - 调用deliverKeyEventToViewHierarchy(),将消息派发给真正的视图。该函数内部又分为四步: + - 调用checkForLeavingTouchModeAndConsume()判断该消息是否会导致离开触摸模式,并且会消耗掉该消息,一般情况下该函数总是会返回false。 + - 调用mView.dispatchKeyEvent()将消息派发给根视图。对于应用窗口而言,根视图就是PhoneWindow的DecorView对象。而DecorView的dispatchKeyEvent内部会判断去处理音量键、系统快捷键等 + - 如果应用程序中没有处理该消息,则默认会判断该消息是否会引起视图焦点的变化,如果会就进行焦点切换。 + + + + + +和按键派发类似,当消息获取模块通过pipe将消息传递给客户端,InputQueue中的next()函数内部调用nativePollOnce()函数中会读取该消息,如果有消息,则回调ViewRoot内部的mInputHandler对象的dispatchMothion()函数,该函数仅仅是发起一个DISPATCH_POINTER异步消息,消息的处理函数是deliverPointerEvent()。执行完该函数后,调用finishInputEvent()向消息获取模块发送一个回执,以便其进行下一次消息派发。 + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/OperatingSystem/AndroidKernal/6.\345\261\217\345\271\225\347\273\230\345\210\266\345\237\272\347\241\200.md" "b/OperatingSystem/AndroidKernal/6.\345\261\217\345\271\225\347\273\230\345\210\266\345\237\272\347\241\200.md" new file mode 100644 index 00000000..b4f24bb7 --- /dev/null +++ "b/OperatingSystem/AndroidKernal/6.\345\261\217\345\271\225\347\273\230\345\210\266\345\237\272\347\241\200.md" @@ -0,0 +1,51 @@ +# 6.屏幕绘制基础 + +Android中的GUI系统是客户端和服务端配合的窗口系统,即后台运行了一个绘制服务,每个应用程序都是该服务端的一个客户端,当客户端需要绘制时,首先请求服务端创建一个窗口,然后在窗口中进行具体的视图内容绘制;对于每个客户端而言,他们都感觉自己独占了屏幕,而对于服务端而言,它会给每一个客户端窗口分配不同的层值,并根据用户的交互情况动态改变窗口的层值,这就给用户造成了所谓的前台窗口和后台窗口的概念; + +Android的屏幕绘制架构如下图: + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_draw_process_archi.jpg) + +- SurfaceFlinger服务进程:简称sf。该进程在整个系统中只有一个实例,在系统开机后自动运行,它的作用是给每个客户端分配窗口,程序中用Surface类表示这个窗口。正如Surface字面的意义,它是一个平面,即每个窗口是一个平面,每个平面在程序中都对应一段内存,也就是所谓的屏幕缓冲区,不同窗口的缓冲区大小不同,这取决于该窗口的大小,即宽度和高度,一般来讲缓冲区的大小为宽度x高度。sf的客户端必须使用SurfaceFlinger的客户端接口驱动来和sf打交道,系统中使用该接口驱动的最重要的进程就是SystemServer进程。 +- 当一个APK程序需要创建窗口时,会使用WindowManager类的addView()函数,该函数会创建一个ViewRoot对象,而ViewRoot类中会使用Surface的无参构造函数创建一个Surface对象,此时该Surface仅仅是一个空壳,然后调用WindowManager类向WmS服务发起一个请求,请求的参数中包含该Surface对象。虽然它表示的是一个窗口,但它必须经过初始化后才真正能够对应一个再屏幕上显示的窗口,而初始化的本质就是给该Surface对象分配一段屏幕缓冲区的内存。 +- WmS收到这个请求后,会通过Surface类的JNI调用到SurfaceFlinger_client驱动,通过该client接口驱动请求sf进程创建指定的窗口。于是sf创建一段屏幕缓冲区,并在sf内部记录该窗口,然后sf会把该窗口的缓冲区地址传递给WmS,WmS再用这个地址去初始化APK程序传入的Surface对象,并最终回到APK程序中。此时APK程序中的Surface对象是一个真正的Surface对象了,因为它包含的屏幕缓冲区已经由sf创建并备案了。 +- APK程序有了这个Surface后,就可以给这个平面上绘制任意的内容了,比如绘制矩阵、绘制文本、绘制图片等。然后Surface类本质上仅仅表示了一个平面,而绘制不同图片显然是一种操作,而不是一段数据,因此Android中使用了一个叫做Skia的绘图驱动库,该库使用C/C++语言编写而成,其作用就是能够进行各种平面绘制。在程序中用Canvas类来表示这个功能对象,Canvas类有很多绘制函数,比如drawColor()、drawLine()等。Surface类包含了一个函数lockCanvas(),APK应用程序可以通过该函数返回一个Canvas功能对象,然后就可以调用该对象的各种绘制函数完成对平面的绘制。 + + + +### 相关类 + +- Surface类:该类用于描述一个绘制平面,其内部仅仅包含了该平面的大小,在屏幕上的位置,以及一段屏幕缓冲内存区。不过在Java端,不能直接访问这段内存,同时也不能通过该类直接设置平面的大小和位置,而只能通过SurfaceHolder类。 + + 一般情况下,客户端程序对应的Surface都是由底层的ViewRoot类进行创建的,而ViewRoot中创建Surface的函数在SDK中没有开放,所以应用程序不能通过ViewRoot类直接创建Surface对象,而只能通过SurfaceView类间接创建。 + + 对于SDK开发的APK程序而言,不能直接创建Surface对象,而只能使用WindowManager类的addView()方法创建一个窗口,因为Surface的构造函数都是@hide的,即不会出现在SDK中。Surface类有两个构造函数: + + - public Surface() {} + + 使用该构造函数创建的Surface对象仅仅是一个空壳,因为每个Surface内部都会对应一段屏幕缓冲区内存,对于空壳子Surface而言,这段内存不存在。 + + - public Surface(SurfaceSession s, int pid, int display, int w, int h, int format, int flags) + + 该构造函数包含窗口大小相关的参数,使用该构造函数会创建一个真正的Surface对象。 + + WindowManager类的addView()函数会创建一个ViewRoot对象,而ViewRoot类则使用Surface的无参数构造函数创建了一个Surface对象,此时该Surface是一个空壳,然后ViewRoot类调用WmS中的IWindowSession服务为该Surface对象分配真正的屏幕缓冲区内容。之后Surface的构造函数回去创建一个Canvas对象,然后调用init()函数来初始化该Surface对象,该函数内部会首先获得一个SurfaceComposerClient对象,该类正式native层面上的SurfaceFlinger服务的客户端对象,然后调用该对象的createSurface()函数创建一个真正的Surface对象。之后调用Surface的lock()函数获取一个SurfaceInfo对象。该SurfaceInfo对象中获取该Surface对应屏幕缓冲区内存地址。再用这个地址构造一个SkBitmap对象,之后用SkBitmap对象构造一个SkCanvas对象,这个SkCanvas对象是底层真正进行绘制的功能类对象,Java层面的Canvas类仅仅是该类的包装而已,之后将Canvas对象返回到Java端。 + +- Canvas类:是一个功能类,该类包含各种绘制函数,例如drawColor()、drawLine()等。构造Canvas对象时,必须为该Canvas指定一段内存地址,因为绘制的结果实际上就是给这段内存地址中填充不同的像素值。这段内存有两种类型,一种是普通的内存,另一种是屏幕缓冲区内存,当Canvas对应的内存为屏幕缓冲区内存时,绘制函数执行后就可以在屏幕上看到,如果是一段普通内存,则不会在屏幕上看到,不过却可以将这段内存复制到拥有屏幕缓冲内存的Canvas中,这种方式就是游戏开发中常用的方式。 + +- Drawable类:是一个抽象类,该类是一个功能类,但它与Canvas的相同之处是两者可以给内存缓冲区中绘制图案,两者的区别有两点: + + - Drawable类内部不存在一段内存缓冲区,当应用程序需要绘制某种图案时,可以将一个包含内存缓冲区的Canvas对象传递给Drawable,然后Drawable就可以给该Canvas上绘制相应的团。 + - 每个具体的Drawable对象仅仅绘制某个特定的图案,SDK中包含的Drawable实现类有BitmapDrawable、NinePatchDrawable等。 + + + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/OperatingSystem/AndroidKernal/7.View\347\273\230\345\210\266\345\216\237\347\220\206.md" "b/OperatingSystem/AndroidKernal/7.View\347\273\230\345\210\266\345\216\237\347\220\206.md" new file mode 100644 index 00000000..a5def400 --- /dev/null +++ "b/OperatingSystem/AndroidKernal/7.View\347\273\230\345\210\266\345\216\237\347\220\206.md" @@ -0,0 +1,16 @@ +# 7.View绘制原理 + + + + + + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/OperatingSystem/AndroidKernal/8.WindowManagerService\347\256\200\344\273\213.md" "b/OperatingSystem/AndroidKernal/8.WindowManagerService\347\256\200\344\273\213.md" new file mode 100644 index 00000000..7fa274dc --- /dev/null +++ "b/OperatingSystem/AndroidKernal/8.WindowManagerService\347\256\200\344\273\213.md" @@ -0,0 +1,131 @@ +# 8.WindowManagerService简介 + + +WmS是Android中图形用户接口的引擎,它管理者所有窗口,包括创建、删除窗口以及将某个窗口设置为焦点等。 + +在WmS中,窗口是由两部分内容构成: + +- 描述该窗口的类WindowState。 +- 该窗口在屏幕上对应的界面Surface。 + +它的功能可以归纳为两个: + +- 保持窗口的层次关系,以便SurfaceFlinger能够据此绘制屏幕 +- 把窗口信息传递给InputManager对象,以便InputDispatcher能够把输入消息派发给和屏幕上显示一致的窗口。 + +Android采用层叠式布局,这种布局的特点在于允许多个窗口层叠显示。该布局一般都需要一个窗口管理服务端。从程序设计的角度看,有两种设计模式可以实现服务端: + +- 独立进程方式 + + 使用一个独立的进程专门用于屏幕的绘制和消息处理,所有的其他引用程序当需要创建窗口时,通过进程通信的方式请求管理服务创建窗口。比如Linux上的X-window就是一种独立进程的方式,它使用Socket通信的方式,通知窗口管理服务进行窗口的创建及交互消息传递。 + +- 共享库方式 + + 使用一段共享程序,该段共享程序中保存了所有客户端的窗口信息,共享库和每个客户端程序都运行于同一个进程之间。Windows操作系统使用的就是这种方式,很多嵌入式系统也使用这种方式。该方式的有点是窗口管理的开销比较小,尤其是窗口的交互,因为它不需要进程间通信,其缺点就是任何一个客户端的不适当操作都可能导致窗口系统崩溃。 + + + + + +在WmS内部逻辑中,会进行三种常见的操作,具体的操作可能会对应不同的函数名称,这三种常见的操作为assign layer、perform layout以及place surface: + +- Assign layer的意思是为窗口分配层值。在WmS中,每个窗口都是用WindowState类来描述,而窗口要在界面上显示时,需要制定窗口的层值。从用户的视角来看,层值越大,其窗口越靠近用户,窗口之间的层叠正式按照层值进行的。 +- perform layout的意思是计算窗口的大小。每个窗口对应都必须有一个大小,即窗口大小,perform layout将根据状态栏大小、输入法窗口的状态、窗口动画状态计算该窗口的大小。 +- place surface的意思是调整Surface对象的属性,并重新将其显示到屏幕上。由于assign layer和perform layout的执行结果影响的仅仅是WindowState中的参数,而能够显示到屏幕上的窗口都包含一个Surface对象,因此只有将以上执行结果的窗口层值、大小设置到Surface对象中,屏幕上才能看出该窗口的变化。place surface的过程就是将这些值赋值给Surface对象,并告诉Surface Flinger服务重新显示这些Surface对象。 + + + +### WmS接口结构 + +WmS接口结构是指WmS功能模块与其他模块之间的交互接口,其中主要包括与AmS模块及应用程序客户端的接口, + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/wms_api.png) + +该结构中的主要交互过程如下: + +- 应用程序在Activity中添加、删除窗口。具体实现就是通过调用WindowManager类的addView()和removeView()函数完成,这会转而调用ViewRoot类的相关方法,然后通过IPC调用到WmS中的相关方法完成添加、删除过程。 +- 当AmS通知ActivityThread销毁某个Activity时,ActivityThread会直接调用WindowManager中的removeView()方法删除窗口。 +- AmS中直接调用WmS,这种调用一般都不是请求WmS创建或删除窗口,而是告诉WmS一些其他信息。 + +在WmS内部,全权接管了输入消息的处理和屏幕的绘制。其中输入消息的处理是借助于InputManager类完成的。而绘制屏幕则是借助于SurfaceFlinger模块完成的,SurfaceFlinger是Linux的一个驱动,它内部会使用芯片的图形加速引擎完成对界面的绘制。 + + + +### WindowState + +由于WmS是用来管理窗口的,因此需要定义一个专门的类来表示窗口,WmS中表示窗口的类就是WindowState。从设计原理的角度来说,似乎使用WindowState类来表示一个窗口就可以了,但是从程序实现的角度来讲,为了变成的便利性及程序逻辑的清晰性,WmS类内部还定义了两个额外的用来表示窗口的类,分别是WindowToken和AppWindowToken。为什么还需要这两个额外的类? + +- 每个窗口都会对应一个WindowState对象。因为窗口的本质就是由WindowState类描述的数据对象,WindowState类中记录作为一个窗口应该有的全部属性,比如窗口的大小,在屏幕上的层值,以及窗口动画过程的各种状态信息。 +- WindowToken描述的是窗口对应的token的相关属性,每个窗口都会对应一个WindowToken对象,但是一个窗口的所有子窗口将对应同一个WindowToken对象,即多对一的关系。 +- 如果窗口是由Activity创建的,即该窗口对应一个Activity,那么该窗口同时对应一个AppWindowToken对象。 + + + +### Session + +和SurfaceFlinger直接打交道的类本来是SurfaceSession。当应用程序需要创建Surface时,会请求WmS去完成创建的工作,WmS回味每一个应用程序分配一个SurfaceSession对象。然而一个surfaceSession对象不足以表示一个客户端,因此,WmS定义了Session类,它可被认为是SurfaceSession的一个包装。Session对象是当应用程序调用WmS的openSession()函数时创建的,而应用程序又是在ViewRoot类中调用openSession的。 + + + + + +## 创建窗口 + + + +创建窗口的时机可分为两种: + +- 程序员主动调用WindowManager类的addView()方法。 +- 当用户启动一个新的Activity或者显示一个对话框、菜单栏等的时候,在这种情况下,程序员并不是直接调用addView()函数,但是这些类的内部同样会间接调用addView()函数。 + + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/create_window_process.png) + +- 客户端调用WindowManager类的addView()方法后,该方法会创建一个新的ViewRoot对象,然后调用ViewRoot类的setView()方法,该方法中会通过IPC方式调用WmS类中内联类Session的add()方法。 +- Session类的add()方法又会间接调用WmS的addWindow()方法,该方法内部又分为三个小过程: + - 第一个过程是进行前置处理,即首先判断参数的合法性,以确保接下来的添加操作能够顺利进行。 + - 第二个过程是具体添加和窗口相关的数据。 + - 第三个过程是后置处理,即添加窗口会引起相关状态的变化,因此需要把这些变化反应到相关的数据中。 + + + + + +## AmS与WmS的交互 + + + +AmS和WmS都是窗口管理系统的核心,不过AmS侧重于对Activity的管理,而WmS侧重于对窗口的管理。系统启动后首先是由AmS接管主控制权力,然后AmS开始调度并运行Activity,WmS作为AmS的辅助服务,接受应用程序的请求创建窗口。当窗口显示后,用户和窗口的交互控制则交由WmS和应用程序本身来完成,而当应用程序需要启动新的Activity时,则又交给AmS去处理,系统就这样周而复始的运行。 + + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/ams_wms.png) + +以上过程是在两个进程、三个独立线程中异步完成的,从代码的角度解释以上执行过程: + +- 当要启动B时,AmS会调用WmS的addAppToken()添加一个token,该token对应的是新的Activity。然后再调用WmS的setAppStartingWindow()告诉WmS启动窗口的标题和图标,以便WmS能根据这两个信息创建一个启动窗口。WmS接收到这个命令后就开始去创建启动窗口了,创建的具体过程就是标准的添加窗口的过程。 +- 与此同时,AmS还去启动B对应的进程,如果进程已经存在,则运行一个ActivityThread实例。每个Activity都是从ActivityThread开始执行的,ActivityThread类是客户端程序的主类,Activity仅仅是一个回调而已。 +- 在接下来的一段时间里,AmS处于空闲状态。WmS内部则开始创建启动窗口,并可能已经创建完毕了启动窗口,但暂时不能显示该启动窗口。而ActivityThread内部也忙碌的启动进程并使ActivityThread就绪。 +- 当ActivityThread就绪后,就会通过IPC调用AmS的attachApplication(),通知AmS自己已经就绪,可以运行任何指定的Activity了。当AmS收到这个通知后,一方面会调用WmS的setAppVisibility()使其开始显示启动窗口,并调用WmS中的setFocusedApp()将新的AppWindowToken设为焦点窗口,然而此时由于真正的窗口还没有就绪,所以焦点窗口被调整为Null。另一方面则调用ActivityThread中内联类ApplicationThread的scheduleLaunchActivity(),请求其开始运行指定的Activity,这最终会调用执行到Activity类的onCreate()函数中。 +- 在接下来的一段时间里,在WmS中,由于真正的Activity窗口还没有被创建,因此当前的焦点窗口被调整为null,并且开始了启动动画。另一方面,在ActivityRecord类中则开始运行Activity的onCreate(),该函数最终会调用到setContentView(),这会间接的创建一个真正的Activity窗口。 +- 当ActivityThread内部执行到创建真正的Activity窗口时,会调用到WmS中的addWindow()函数,在该函数中,当添加完新窗口后,就会把焦点调整到新窗口中。 + + + +## 销毁Surface的过程 + +Surface的销毁有两种情况: + +- AmS调用WmS的setAppVisibility()设置指定应用窗口的可视状态。该方法会调用到relayoutWindow()方法,这里面会销毁窗口对应的Surface。 +- 当要删除窗口时,这个很容易理解,窗口都不在了,内部的Surface自然应该被销毁。 + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/OperatingSystem/AndroidKernal/9.PackageManagerService\347\256\200\344\273\213.md" "b/OperatingSystem/AndroidKernal/9.PackageManagerService\347\256\200\344\273\213.md" new file mode 100644 index 00000000..803a99df --- /dev/null +++ "b/OperatingSystem/AndroidKernal/9.PackageManagerService\347\256\200\344\273\213.md" @@ -0,0 +1,65 @@ +# 9.PackageManagerService简介 + +程序包管理主要包含三部分内容: + +- 提供一个能够根据intent匹配到具体的Activity、Provider、Service。即当应用程序调用startActivity(intent)时,能够把参数中指定的intent转换成一个具体的包含了程序包名称及具体Component名称的信息,以便Java类加载器加载具体的Component。 +- 进行权限检查。即当应用程序调用某个需要一定权限的函数调用时,系统能够判断调用者是否具备该权限,从而保证系统的安全。 +- 提供安装、删除应用程序接口。 + + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/package_manager_service_archi.png) + +该框架可以分为三层: + +- 应用程序层 + + 应用程序需要使用包管理服务时,调用ContextImpl类的getPackageManager()函数返回一个PackageManager对象,然后调用该对象所提供的各种API接口。 + +- PmS服务层 + + 和AmS、WmS等其他系统服务一样,包管理服务运行于SystemServer进程。PmS服务运行时,使用了两个目录下的XML文件保存相关的包管理信息。 + + - 第一个目录是system/etc/permissions:该目录下的所有xml文件用于permission的管理,具体包含两件时间。第一个是定义系统中都包含了哪些feature,应用程序可以在AndroidManifest.xml中使用use-feature标签声明程序都需要哪些feature。 + - 第二个目录是/data/system/packages.xml,该文件保存了所有安装程序的基本包信息,有点像系统的注册表,比如程序的包名称是什么,安装包路径在哪里,程序都是用了哪些系统权限。 + + PmS在启动时,会从这两个目录中解析相关的XML文件,从而建立一个庞大的包信息树,应用程序可以间接从这个信息树中查询所有所需的程序包信息。 + + 除了PmS服务外,还有两个辅助系统服务用于程序安装。 一个是DefaultContainerService,该服务主要用于把安装程序复制到程序目录中。另一个是Installer服务,该服务实际上并不是一个Binder,而是一个Socket客户端,PmS直接和该Socket客户端交互。Socket的服务端主要完成程序文件的解压工作及数据目录创建,比如从APK文件中提取dex文件,删除dalvik-cache(会把每个apk的dex文件放到该目录,方便提升执行速度)目录下的dex文件,创建程序专属的程序目录等。 + +- 数据文件层 + + 就像所有操作系统一样,Android中的程序也由相关的程序文件组成,这些程序文件可以分为三个部分: + + - 程序文件 + + 所有的系统程序保存在/system/app目录下,所有的第三方应用程序保存在/data/app目录下,该目录中的APK与原始的APK文件的唯一区别是文件的名称不同,原始文件可以任意命名,而该目录下的文件名称是以包名进行命名,并自动增加一个"-x"后缀,比如com.android.haii.debugjar-1.apk。当同样一个程序第二次安装时,后面的数字1会变成数字2,而当第三次再安装时,又会变成数字1,有点像“乒乓”机制。。/data/dalvik-cache目录保存了程序中的执行代码。一个APK实际上是一个Jar压缩类型的文件,压缩包中包含了各种资源文件、资源索引文件、AndroidManifest文件及程序文件,当应用程序运行前,PmS会从APK文件中提取出代码文件,也就是所谓的dex文件,并将该文件存储在该目录下,以便以后能够快速运行该程序。比如: data@app@com.android.xxx-1.apk@classes.dex + + - framework库文件 + + 这些库文件存在于/system/framework目录下,库文件类型是APK或者Jar,系统开机后,dalvik虚拟机会加载这些库文件,而在PmS启动时,如果这些Jar或者APK文件还没有被转换为dex文件,则PmS会将这些库文件转换为dex文件,并保存到/data/dalvik-cache目录下。 + + - 应用程序所使用的数据文件 + + 应用程序可以使用三种数据保存方式,分为为参数存储、数据库存储、文件存储。这三种存储方式对应的数据文件一般都保存到/data/data/xxx目录下,xxx代表程序的包名。 + + + +安装及卸载程序的操作都是由PmS完成,安装程序的过程包括在程序目录下创建以包名称命名的程序文件、创建程序数据目录,以及把程序信息保存到相关的配置文件packages.xml中,卸载则是一个相反的操作。 + + + + + +### aipalign优化APK内部存储 + +所谓的内部存储优化是指,为了提高APK程序的加载速度,从而对APK中相关的数据进行边界对齐。因为从底层NAND Flash的角度来讲,读取NAND时,是以一个扇区进行读取的,因此,如果相关的数据能够在同一个扇区中,肯定会提高读取速度。zipalign的作用正是将APK包中的不同类型的数据文件进行边界对齐。 + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! + diff --git "a/SourceAnalysis/Activity\345\220\257\345\212\250\350\277\207\347\250\213.md" "b/SourceAnalysis/Activity\345\220\257\345\212\250\350\277\207\347\250\213.md" index 43ba62de..cc1cdbb3 100644 --- "a/SourceAnalysis/Activity\345\220\257\345\212\250\350\277\207\347\250\213.md" +++ "b/SourceAnalysis/Activity\345\220\257\345\212\250\350\277\207\347\250\213.md" @@ -2,6 +2,10 @@ Activity启动过程 === 前两天面试了天猫的开发,被问到了`Activity`启动过程,不懂啊.... + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/app launch summary.jpg) + + 今天就来分析一下,我们开启`Activity`主要有两种方式: - 通过桌面图标启动,桌面就是`Launcher`其实他也是一个应用程序,他也是继承`Activity`。 diff --git "a/SourceAnalysis/Android Touch\344\272\213\344\273\266\345\210\206\345\217\221\350\257\246\350\247\243.md" "b/SourceAnalysis/Android Touch\344\272\213\344\273\266\345\210\206\345\217\221\350\257\246\350\247\243.md" index 66fb66eb..1ef073c8 100644 --- "a/SourceAnalysis/Android Touch\344\272\213\344\273\266\345\210\206\345\217\221\350\257\246\350\247\243.md" +++ "b/SourceAnalysis/Android Touch\344\272\213\344\273\266\345\210\206\345\217\221\350\257\246\350\247\243.md" @@ -3,27 +3,35 @@ Android Touch事件分发详解 先说一些基本的知识,方便后面分析源码时能更好理解。 - 所有`Touch`事件都被封装成`MotionEvent`对象,包括`Touch`的位置、历史记录、第几个手指等. - - 事件类型分为`ACTION_DOWN`,`ACTION_UP`,`ACTION_MOVE`,`ACTION_POINTER_DOWN`,`ACTION_POINTER_UP`,`ACTION_CANCEL`, 每个 一个完整的事件以`ACTION_DOWN`开始`ACTION_UP`结束,并且`ACTION_CANCEL`只能由代码引起.一般对于`CANCEL`的处理和`UP`的相同。 `CANCEL`的一个简单例子:手指在移动的过程中突然移动到了边界外,那么这时`ACTION_UP`事件了,所以这是的`CANCEL`和`UP`的处理是一致的。 - -- 事件的处理分别为`dispatchTouchEveent()`分发事件(`TextView`等这种最小的`View`中不会有该方式)、`onInterceptTouchEvent()`拦截事件(`ViewGroup`中拦截事件)、`onTouchEvent()`消费事件. - +- 事件的处理分别为`dispatchTouchEveent()`分发事件(`TextView`等这种最小的`View`中不会有该方式)、`onInterceptTouchEvent()`拦截事件(`ViewGroup`中拦截事件)、`onTouchEvent()`消费事件.这些方法的返回值如果是true表示事件被当前视图消费掉。 - 事件从`Activity.dispatchTouchEveent()`开始传递,只要没有停止拦截,就会从最上层(`ViewGroup`)开始一直往下传递,子`View`通过`onTouchEvent()`消费事件。(隧道式向下分发). - -- 如果时间从上往下一直传递到最底层的子`View`,但是该`View`没有消费该事件,那么该事件会反序网上传递(从该`View`传递给自己的`ViewGroup`,然后再传给更上层的`ViewGroup`直至传递给`Activity.onTouchEvent()`). +- 如果时间从上往下一直传递到最底层的子`View`,但是该`View`没有消费该事件(不是clickable或longclickable),那么该事件会反序网上传递(从该`View`传递给自己的`ViewGroup`,然后再传给更上层的`ViewGroup`直至传递给`Activity.onTouchEvent()`). (冒泡式向上处理). - - 如果`View`没有消费`ACTION_DOWN`事件,之后其他的`MOVE`、`UP`等事件都不会传递过来. - - 事件由父`View(ViewGroup)`传递给子`View`,`ViewGroup`可以通过`onInterceptTouchEvent()`方法对事件进行拦截,停止其往下传递,如果拦截(返回`true`)后该事件 会直接走到该`ViewGroup`中的`onTouchEvent()`中,不会再往下传递给子`View`.如果从`DOWN`开始,之后的`MOVE`、`UP`都会直接在该`ViewGroup.onTouchEvent()`中进行处理。 如果子`View`之前在处理某个事件,但是后续被`ViewGroup`拦截,那么子`View`会接收到`ACTION_CANCEL`. +- `OnTouchListener`优先于`onTouchEvent()`对事件进行消费,而`onTouchEvent()`又优先于`onCickListener.onClick()`。 +- `TouchTarget`是保存手指点击区域属性的一个类,手指的所有移动过程都会被它记录下来, 包含被`touch`的`View`。 +- ViewGroup默认不拦截任何事件,返回false。 +- View的onTouchEvent默认都会消费事件,返回true,除非它是不可点击的(clickable和longclickable都为false),View的longClickable默认都是false,clickable对于Button等为true,而TextView等为false。 +- View的enable属性不影响onTouchEvent的默认返回值。 +- 通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。 + +在Android系统中,拥有事件传递处理能力的类有以下三种: + +- Activity:拥有分发和消费两个方法。 +- ViewGroup:拥有分发、拦截和消费三个方法。 +- View:拥有分发、消费两个方法。 + + + +对触摸屏进行操作时,Linux就会收到相应的硬件中断,然后将中断加工成原始的输入事件并写入相应的设备节点中。而Android输入系统所做的事情概括起来说就是监控这些设备节点,当某个设备节点有数据可读时,将数据读出并进行一系列的翻译加工,然后在所有的窗口中找到合适的事件接收者,并派发给它。当点击事件产生后,事件会传递给当前的Activity,由Activity中的PhoneWindow处理,PhoneWindow再把事件处理工作交给DecorView,之后再由DecorView将事件处理工作交给ViewGroup。 -- `OnTouchListener`优先于`onTouchEvent()`对事件进行消费。 -- `TouchTarget`是保存手指点击区域属性的一个类,手指的所有移动过程都会被它记录下来, 包含被`touch`的`View`。 废话不多说,直接上源码,源码妥妥的是最新版5.0: 我们先从`Activity.dispatchTouchEveent()`说起: @@ -43,9 +51,11 @@ public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } + // 首先交给本Activity对应的Window来进行分发,如果分发了,就返回true,事件循环结束 if (getWindow().superDispatchTouchEvent(ev)) { return true; } + // 如果window返回了false,就意味着所有view的ontouchevent都返回了false,那么只能是Activity来决定消费不消费 return onTouchEvent(ev); } ``` @@ -97,8 +107,9 @@ private final class DecorView extends FrameLayout implements RootViewSurfaceTake ... } ``` -它集成子`FrameLayout`所有很多时候我们在用布局工具查看的时候发现`Activity`的布局`FrameLayout`的。就是这个原因。 +它继承自`FrameLayout`所有很多时候我们在用布局工具查看的时候发现`Activity`的布局`FrameLayout`的。就是这个原因。 好了,我们接着看`DecorView`中的`superDispatchTouchEvent()`方法。 + ```java public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); @@ -134,6 +145,7 @@ public boolean dispatchTouchEvent(MotionEvent ev) { // The framework may have dropped the up or cancel event for the previous gesture // due to an app switch, ANR, or some other state change. cancelAndClearTouchTargets(ev); + // 重置FLAG_DISALLOW_INTERCEPT resetTouchState(); // 如果是`Down`,那么`mFirstTouchTarget`到这里肯定是`null`.因为是新一系列手势的开始。 // `mFirstTouchTarget`是处理第一个事件的目标。 @@ -142,10 +154,12 @@ public boolean dispatchTouchEvent(MotionEvent ev) { // 检查是否拦截该事件(如果`onInterceptTouchEvent()`返回true就拦截该事件) // Check for interception. final boolean intercepted; + // 当事件由ViewGroup的子元素成功处理时,mFirstTouchTarget会被赋值并指向子元素,反之被ViewGroup拦截时,mFirstTouchTarget则为null if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { // 标记事件不允许被拦截, 默认是`false`, 该值可以通过`requestDisallowInterceptTouchEvent(true)`方法来设置, - // 通知父`View`不要拦截该`View`上的事件。 + // 通知父`View`不要拦截该`View`上的事件。FLG_DISALLOW_INTERCEPT是在View中通过 + // reqeustDisallowInterceptTouchEvent来设置 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { // 判断该`ViewGroup`是否要拦截该事件。`onInterceptTouchEvent()`方法默认返回`false`即不拦截。 @@ -339,7 +353,7 @@ public boolean dispatchTouchEvent(MotionEvent ev) { } return handled; } -``` +``` 接下来还要说说`dispatchTransformedTouchEvent()`方法,虽然上面也说了大体功能,但是看一下源码能说明另一个问题: ```java @@ -511,8 +525,7 @@ public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } - // A disabled view that is clickable still consumes the touch - // events, it just doesn't respond to them. + // 只要view的clickable和long_clickable有一个是true,onTouchEvent就会返回true消耗这个事件。 return (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)); } @@ -708,7 +721,7 @@ public boolean performClick() { 讲到这里就明白了。`onTouchEvent()`中的`ACTION_UP`中会调用`performClick()`方法。 -到这里,就全部分析完了,这一块还是比较麻烦的,中间查了很多资料,有些地方自己可能也理解的不太对,如果有哪里理解的不对的地方,还请大家指出来。谢谢。 +到这里,就全部分析完了。 --- diff --git "a/SourceAnalysis/View\347\273\230\345\210\266\350\277\207\347\250\213\350\257\246\350\247\243.md" "b/SourceAnalysis/View\347\273\230\345\210\266\350\277\207\347\250\213\350\257\246\350\247\243.md" index 57e2712b..e4168121 100644 --- "a/SourceAnalysis/View\347\273\230\345\210\266\350\277\207\347\250\213\350\257\246\350\247\243.md" +++ "b/SourceAnalysis/View\347\273\230\345\210\266\350\277\207\347\250\213\350\257\246\350\247\243.md" @@ -3,6 +3,7 @@ View绘制过程详解 界面窗口的根布局是`DecorView`,该类继承自`FrameLayout`.说到`View`绘制,想到的就是从这里入手,而`FrameLayout`继承自`ViewGroup`。感觉绘制肯定会在`ViewGroup`或者`View`中, 但是木有找到。发现`ViewGroup`实现`ViewParent`接口,而`ViewParent`有一个实现类是`ViewRootImpl`, `ViewGruop`中会使用`ViewRootImpl`... + ```java /** * The top of a view hierarchy, implementing the needed protocol between View @@ -18,13 +19,23 @@ public final class ViewRootImpl implements ViewParent, } ``` -`View`的绘制过程从`ViewRootImpl.performTraversals()`方法开始。 +`View`的绘制过程从`ViewRootImpl.performTraversals()`方法开始,你看这个名字起的多好,叫执行遍历,看到名字就能知道内部的实现: + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/view_performTraversals.png) + 首先先说明一下,这部分代码比较多,逻辑也比较麻烦,很容易弄晕,如果感觉看起来费劲,就跳过这一块,直接到下面的Measure、Layout、Draw部分开始看。 我也没有全部弄清楚,我只是把里面的步骤标注了下。 + ```java private void performTraversals() { // ... 此处省略源代码N行 - + if (mFirst || windowShouldResize || insetsChanged || + viewVisibilityChanged || params != null || mForceNextWindowRelayout) { + // 第一或者resize等都会调用relayoutWindow,而该函数内部会调用sWindowSession.relayout() + // 方法来请求WmS按照指定的大小重新分配窗口大小,并会为客户窗口创建的mSurface对象分配真正的现存 + // 等该函数返回后,应用程序就可以在该Surface中绘制了。 + relayoutResult = relayoutWindow(params, viewVisibility, insetsPending); + } // 是否需要Measure if (!mStopped) { boolean focusChangedDueToTouchMode = ensureTouchModeLocally( @@ -35,6 +46,7 @@ private void performTraversals() { // getRootMeasureSpec方法内部会使用MeasureSpec.makeMeasureSpec()方法来组装一个MeasureSpec, // 当lp.width参数等于MATCH_PARENT的时候,MeasureSpec的specMode就等于EXACTLY,当lp.width等于WRAP_CONTENT的时候,MeasureSpec的specMode就等于AT_MOST。 // 并且MATCH_PARENT和WRAP_CONTENT时的specSize都是等于windowSize的,也就意味着根视图总是会充满全屏的。 + // 这里lp代表的是根视图的LayoutParams、lp.width和lp.height直接来源于用户的定义比如WRAP_CONTENT、MATCH_PARENT等 int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); @@ -159,7 +171,14 @@ private void performTraversals() { `Measure` === + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/view_measure.png) + + + `performMeasure`方法如下: + ```java private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure"); @@ -199,6 +218,7 @@ public final void measure(int widthMeasureSpec, int heightMeasureSpec) { Insets insets = getOpticalInsets(); int oWidth = insets.left + insets.right; int oHeight = insets.top + insets.bottom; + // adjust是微调某个MeasureSpec的大小 widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth); heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight); } @@ -556,7 +576,14 @@ ps:譬如我们设置了`setMeasuredDimension(10, 10)`,那么不管布局中怎 `Layout` === + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/view_layout.png) + + + `performLayout`方法源码如下: + ```java private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) { @@ -598,6 +625,7 @@ private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth " during layout: running second layout pass"); view.requestLayout(); } + // desiredWindowWidth和desiredWindowHeight是屏幕的尺寸 measureHierarchy(host, lp, mView.getContext().getResources(), desiredWindowWidth, desiredWindowHeight); mInLayout = true; @@ -825,7 +853,12 @@ private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth `Draw` === + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/view_draw.png) + 绘制阶段是从`ViewRootImpl`中的`performDraw`方法开始的: + ```java private void performDraw() { if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) { @@ -887,6 +920,7 @@ private void performDraw() { ```java private void draw(boolean fullRedrawNeeded) { Surface surface = mSurface; + // 首先检查surface是否有效,正常情况下都是有效的,除非WmS发生异常不能为该客户端分配有效的Surface if (!surface.isValid()) { return; } @@ -941,6 +975,8 @@ private void draw(boolean fullRedrawNeeded) { } final Rect dirty = mDirty; + // 判断该Surface是否有SurfaceHolder对象,如果有则意味着该Surface是应用程序创建的,因为所有的绘制操作应该由应用程序 + // 自身去负责,于是View系统推出绘制,如果不是,才开始View绘制的内部流程。 if (mSurfaceHolder != null) { // The app owns the surface, we won't draw. dirty.setEmpty(); @@ -982,7 +1018,10 @@ private void draw(boolean fullRedrawNeeded) { } if (!dirty.isEmpty() || mIsAnimating) { + // Surface的底层驱动模式分为两种,一种是使用图形加速支持的Surface,俗称显卡,另一种是使用CPU及内存模拟的Surface。 + // 因此这里需要根据不同的模式,进行不同的操作 if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) { + // 硬件绘制 // Draw with hardware renderer. mIsAnimating = false; boolean invalidateRoot = false; @@ -1023,7 +1062,7 @@ private void draw(boolean fullRedrawNeeded) { return; } - // draw的部分在这里。。。内部会用canvas去画 + // 软件绘制 if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) { return; } @@ -1032,6 +1071,7 @@ private void draw(boolean fullRedrawNeeded) { if (animating) { mFullRedrawNeeded = true; + // 动画就是让画面动起来,如果正在动画过程中,则需要再次发起一个重绘命令,以便接着绘制,直到滚动结束。 scheduleTraversals(); } } @@ -1039,6 +1079,7 @@ private void draw(boolean fullRedrawNeeded) { 我们看一下`drawSoftware`方法: ```java /** + * 使用CPU的软件绘制方式 * @return true if drawing was successful, false if an error occurred */ private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, @@ -1570,6 +1611,10 @@ private void performDraw() { 而且`getMeasureWidth()`的值是通过`setMeasuredDimension()`设置的,但是`getWidth()`的值是通过视图右边的坐标减去左边的坐标计算出来的。如果我们在`layout`的时候将宽高 不传`getMeasureWidth`的值,那么这时候`getWidth()`与`getMeasuredWidth`的值就不会再相同了,当然一般也不会这么干... +# MeasureSpec + +MeasureSpec是View类的一个静态内部类,用来说明如何测量这个类。MeasureSpec表示的是一个32位的整型值,它的高2位表示测量模式SpecMode,低30位表示某种测量模式下的规格大小SpecSize。MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的内存分配。 + --- - 邮箱 :charon.chui@gmail.com From 81e42a8d57dfa487bb1086dcab28954bfbc84074 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 1 Dec 2020 21:39:51 +0800 Subject: [PATCH 037/213] add android kernal --- .../ANR\345\210\206\346\236\220.md" | 66 ------------------- ...345\217\212ANR\345\210\206\346\236\220.md" | 20 ++++-- README.md | 27 +++++++- 3 files changed, 40 insertions(+), 73 deletions(-) delete mode 100644 "AdavancedPart/ANR\345\210\206\346\236\220.md" rename "AdavancedPart/crash\345\210\206\346\236\220.md" => "AdavancedPart/Crash\345\217\212ANR\345\210\206\346\236\220.md" (84%) diff --git "a/AdavancedPart/ANR\345\210\206\346\236\220.md" "b/AdavancedPart/ANR\345\210\206\346\236\220.md" deleted file mode 100644 index fc8ed472..00000000 --- "a/AdavancedPart/ANR\345\210\206\346\236\220.md" +++ /dev/null @@ -1,66 +0,0 @@ -# ANR分析 - -Application Not Responding,字面意思就是应用无响应,稍加解释就是用户的一些操作无法从应用中获取反馈 - - -Android系统中的应用被Activity Manager及Window Manager两个系统服务监控着,Android系统会在如下情况展示出ANR的对话框: -- Service Timeout:比如前台服务在20s内未执行完成;后台服务超过200没有执行 -- BroadcastQueue Timeout:比如前台广播在10s内未执行完成,后台60s -- ContentProvider Timeout:内容提供者,在publish过超时10s -- InputDispatching Timeout: 输入事件分发超时5s,包括按键和触摸事件。 - - - -ANR信息输出到traces.txt文件中 - -traces.txt文件是一个ANR记录文件,用于开发人员调试,目录位于/data/anr中,无需root权限即可通过pull命令获取,下面的命令可以将traces.txt文件拷贝到当前目录下 -adb pull /data/anr . - - -1) Thread基础信息 - -输出种包含所有的线程,取其中的一条 -"Thread-1" prio=5 tid=0x00007fde73872800 nid=0x4a03 waiting for monitor entry [0x000000011cb30000] - java.lang.Thread.State: BLOCKED (on object monitor) - at Test.rightLeft(Test.java:48) - - waiting to lock <0x00000007d56540a0> (a Test$LeftObject) - - locked <0x00000007d5656180> (a Test$RightObject) - at Test$2.run(Test.java:68) - at java.lang.Thread.run(Thread.java:745) -a) "Thread-1" prio=5 tid=0x00007fde73872800 nid=0x4a03 waiting for monitor entry [0x000000011cb30000] - -首先描述了线程名是『Thread-1』,然后prio=5表示优先级,tid表示的是线程id,nid表示native层的线程id,他们的值实际都是一个地址,后续给出了对于线程状态的描述,waiting for monitor entry [0x000000011cb30000]这里表示该线程目前处于一个等待进入临界区状态,该临界区的地址是[0x000000011cb30000] -这里对线程的描述多种多样,简单解释下上面出现的几种状态 - - waiting on condition(等待某个事件出现) - waiting for monitor entry(等待进入临界区) - runnable(正在运行) - in Object.wait(处于等待状态) - -作者:silentleaf -链接:https://www.jianshu.com/p/30c1a5ad63a3 -来源:简书 -著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 - - - - - - - - - - - - - - - - - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! - diff --git "a/AdavancedPart/crash\345\210\206\346\236\220.md" "b/AdavancedPart/Crash\345\217\212ANR\345\210\206\346\236\220.md" similarity index 84% rename from "AdavancedPart/crash\345\210\206\346\236\220.md" rename to "AdavancedPart/Crash\345\217\212ANR\345\210\206\346\236\220.md" index e3dd4ca7..40db0adf 100644 --- "a/AdavancedPart/crash\345\210\206\346\236\220.md" +++ "b/AdavancedPart/Crash\345\217\212ANR\345\210\206\346\236\220.md" @@ -48,10 +48,23 @@ Native Crash -## ANR +# ANR分析 +Application Not Responding,字面意思就是应用无响应,稍加解释就是用户的一些操作无法从应用中获取反馈 +Android系统中的应用被Activity Manager及Window Manager两个系统服务监控着,Android系统会在如下情况展示出ANR的对话框: +- Service Timeout:比如前台服务在20s内未执行完成;后台服务超过200没有执行 +- BroadcastQueue Timeout:比如前台广播在10s内未执行完成,后台60s +- ContentProvider Timeout:内容提供者,在publish过超时10s +- InputDispatching Timeout: 输入事件分发超时5s,包括按键和触摸事件。 + + + +ANR信息输出到traces.txt文件中 + +traces.txt文件是一个ANR记录文件,用于开发人员调试,目录位于/data/anr中,无需root权限即可通过pull命令获取,下面的命令可以将traces.txt文件拷贝到当前目录下 +adb pull /data/anr . ANR排查流程 1、Log获取 @@ -111,10 +124,7 @@ sCount:该线程被挂起的次数 dsCount:该线程被调试器挂起的次数 self:线程本身的地址 -作者:jsonchao -链接:https://juejin.cn/post/6844903972587716621 -来源:掘金 -著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 + diff --git a/README.md b/README.md index 91a59d53..af998819 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,16 @@ Android学习笔记 - [6.文件管理][269] - [7.嵌入式系统][270] - [8.虚拟机][271] + - [Android内核][274] + - [1.Android进程间通信][275] + - [2.Android线程间通信之Handler消息机制][276] + - [3.Android Framework框架][277] + - [4.ActivityManagerService简介][278] + - [5.Android消息获取][279] + - [6.屏幕绘制基础][280] + - [7.View绘制原理][281] + - [8.WindowManagerService简介][282] + - [9.PackageManagerService简介][283] - [架构设计][272] - [1.架构简介][273] @@ -183,7 +193,7 @@ Android学习笔记 - [ApplicationId vs PackageName][77] - [ART与Dalvik][78] - [BroadcastReceiver安全问题][79] - - [Handler导致内存泄露分析][80] + - [Crash及ANR分析][80] - [Library项目中资源id使用case时报错][81] - [Mac下配置adb及Android命令][82] - [MaterialDesign使用][83] @@ -378,7 +388,7 @@ Android学习笔记 [77]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/ApplicationId%20vs%20PackageName.md "ApplicationId vs PackageName" [78]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/ART%E4%B8%8EDalvik.md "ART与Dalvik" [79]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/BroadcastReceiver%E5%AE%89%E5%85%A8%E9%97%AE%E9%A2%98.md "BroadcastReceiver安全问题" -[80]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/Handler%E5%AF%BC%E8%87%B4%E5%86%85%E5%AD%98%E6%B3%84%E9%9C%B2%E5%88%86%E6%9E%90.md "Handler导致内存泄露分析" +[80]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/Handler%E5%AF%BC%E8%87%B4%E5%86%85%E5%AD%98%E6%B3%84%E9%9C%B2%E5%88%86%E6%9E%90.md "Crash及ANR分析" [81]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/Library%E9%A1%B9%E7%9B%AE%E4%B8%AD%E8%B5%84%E6%BA%90id%E4%BD%BF%E7%94%A8case%E6%97%B6%E6%8A%A5%E9%94%99.md "Library项目中资源id使用case时报错" [82]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/Mac%E4%B8%8B%E9%85%8D%E7%BD%AEadb%E5%8F%8AAndroid%E5%91%BD%E4%BB%A4.md "Mac下配置adb及Android命令" [83]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/MaterialDesign%E4%BD%BF%E7%94%A8.md "MaterialDesign使用" @@ -578,6 +588,19 @@ Android学习笔记 [271]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/8.%E8%99%9A%E6%8B%9F%E6%9C%BA.md "8.虚拟机" [272]: https://github.com/CharonChui/AndroidNote/tree/master/Architect "架构设计" [273]: https://github.com/CharonChui/AndroidNote/blob/master/Architect/1.%E6%9E%B6%E6%9E%84%E7%AE%80%E4%BB%8B.md "1.架构简介" +[274]: https://github.com/CharonChui/AndroidNote/tree/master/OperatingSystem/AndroidKernal "Android内核" +[275]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/1.Android%E8%BF%9B%E7%A8%8B%E9%97%B4%E9%80%9A%E4%BF%A1.md "1.Android进程间通信" +[276]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/2.Android%E7%BA%BF%E7%A8%8B%E9%97%B4%E9%80%9A%E4%BF%A1%E4%B9%8BHandler%E6%B6%88%E6%81%AF%E6%9C%BA%E5%88%B6.md "2.Android线程间通信之Handler消息机制" +[277]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/3.Android%20Framework%E6%A1%86%E6%9E%B6.md "3.Android Framework框架" +[278]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/4.ActivityManagerService%E7%AE%80%E4%BB%8B.md "4.ActivityManagerService简介" +[279]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/5.Android%E6%B6%88%E6%81%AF%E8%8E%B7%E5%8F%96.md "5.Android消息获取" +[280]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/6.%E5%B1%8F%E5%B9%95%E7%BB%98%E5%88%B6%E5%9F%BA%E7%A1%80.md "6.屏幕绘制基础" +[281]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/7.View%E7%BB%98%E5%88%B6%E5%8E%9F%E7%90%86.md "7.View绘制原理" +[282]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/8.WindowManagerService%E7%AE%80%E4%BB%8B.md "8.WindowManagerService简介" + +[283]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/9.PackageManagerService%E7%AE%80%E4%BB%8B.md "9.PackageManagerService简介" + + Developed By From 969ea96417418e8461b022b5f7aa9a936ac7e327 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 1 Dec 2020 21:42:28 +0800 Subject: [PATCH 038/213] update readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index af998819..cb8b3b84 100644 --- a/README.md +++ b/README.md @@ -388,7 +388,8 @@ Android学习笔记 [77]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/ApplicationId%20vs%20PackageName.md "ApplicationId vs PackageName" [78]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/ART%E4%B8%8EDalvik.md "ART与Dalvik" [79]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/BroadcastReceiver%E5%AE%89%E5%85%A8%E9%97%AE%E9%A2%98.md "BroadcastReceiver安全问题" -[80]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/Handler%E5%AF%BC%E8%87%B4%E5%86%85%E5%AD%98%E6%B3%84%E9%9C%B2%E5%88%86%E6%9E%90.md "Crash及ANR分析" +[80]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/Crash%E5%8F%8AANR%E5%88%86%E6%9E%90.md "Crash及ANR分析" + [81]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/Library%E9%A1%B9%E7%9B%AE%E4%B8%AD%E8%B5%84%E6%BA%90id%E4%BD%BF%E7%94%A8case%E6%97%B6%E6%8A%A5%E9%94%99.md "Library项目中资源id使用case时报错" [82]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/Mac%E4%B8%8B%E9%85%8D%E7%BD%AEadb%E5%8F%8AAndroid%E5%91%BD%E4%BB%A4.md "Mac下配置adb及Android命令" [83]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/MaterialDesign%E4%BD%BF%E7%94%A8.md "MaterialDesign使用" From 2bb6382ef5ee14cb8d5b58b858bbf32c1f1d0220 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 2 Dec 2020 21:47:15 +0800 Subject: [PATCH 039/213] update README --- ...73\347\273\237\347\256\200\344\273\213.md" | 42 +++++++------- ...13\344\270\216\347\272\277\347\250\213.md" | 57 +++++++------------ ...05\345\255\230\347\256\241\347\220\206.md" | 37 +++++++----- .../4.\350\260\203\345\272\246.md" | 12 ++-- OperatingSystem/5.I:O.md | 8 ++- ...07\344\273\266\347\256\241\347\220\206.md" | 2 + ...45\345\274\217\347\263\273\347\273\237.md" | 2 +- ...8.\350\231\232\346\213\237\346\234\272.md" | 5 +- ...13\351\227\264\351\200\232\344\277\241.md" | 54 +++++++++--------- 9 files changed, 115 insertions(+), 104 deletions(-) diff --git "a/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" index 09bff58c..4e34b27f 100644 --- "a/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" +++ "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" @@ -148,29 +148,29 @@ Linux内核被装载后,就开始进行内核初始化的过程。 ## CPU(Central Processing Unit) -CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从内存中提取指令并执行它。它是一块超大规模的集成电路Integrated Circuit),是信息处理、程序运行的最终执行单元。其功能主要是解释计算机指令以及处理计算机软件中的数据。由于访问内存获取执行或数据要比执行指令花费的时间长,因此所有的 CPU 内部都会包含一些寄存器来保存关键变量和临时结果。因此,在指令集中通常会有一些指令用于把关键字从内存中加载到寄存器中,以及把关键字从寄存器存入到内存中。从功能方面来看,CPU的内部主要由寄存器,控制器,运算器构成,各部分之间由电流信号相互连通。其中运算器负责算术运算和逻辑运算,控制器负责计算指令的解析,产生各种控制指令,寄存器组用来临时存放参加运算的数据和计算的中间结果。CPU计算结果最终需要写到内存中,内存的存取速度远低于CPU,为提升数据交换速率,CPU内部一般还集成了高速缓存(CACHE),其中缓存分为一级缓存和二级缓存,一级缓存和CPU速率相当,二级缓存次之。 +CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从内存中提取指令并执行它。它是一块超大规模的集成电路(Integrated Circuit),是信息处理、程序运行的最终执行单元。其功能主要是解释计算机指令以及处理计算机软件中的数据。由于访问内存获取执行或数据要比执行指令花费的时间长,因此所有的 CPU 内部都会包含一些寄存器来保存关键变量和临时结果。因此,在指令集中通常会有一些指令用于把关键字从内存中加载到寄存器中,以及把关键字从寄存器存入到内存中。从功能方面来看,CPU的内部主要由寄存器,控制器,运算器构成,各部分之间由电流信号相互连通。其中运算器负责算术运算和逻辑运算,控制器负责计算指令的解析,产生各种控制指令,寄存器组用来临时存放参加运算的数据和计算的中间结果。CPU计算结果最终需要写到内存中,内存的存取速度远低于CPU,为提升数据交换速率,CPU内部一般还集成了高速缓存(CACHE),其中缓存分为一级缓存和二级缓存,一级缓存和CPU速率相当,二级缓存次之。 **程序是把寄存器作为对象来描述的** -使用高级语言编写的程序会在编译后转化成机器语言,然后通过CPU内部的寄存器来处理。不同类型的CPU,其内部寄存器的数量,种类以及寄存器存储的数值范围都是不同的。根据功能的不同,我们可以将寄存器大致划分为八类。 +使用高级语言编写的程序会在编译后转化成机器语言,然后通过CPU内部的寄存器来处理。不同类型的CPU,其内部寄存器的数量,种类以及寄存器存储的数值范围都是不同的。根据功能的不同,我们可以将寄存器大致划分为八类: -***累加寄存器简称累加器(Accumulator, AC)***:是一个通用寄存器。存储临时的执行运算的数据和运算后的数据。 +- ***累加寄存器简称累加器(Accumulator, AC)***:是一个通用寄存器。存储临时的执行运算的数据和运算后的数据。 -***标志寄存器***:存储运算处理后的CPU的状态。 +- ***标志寄存器***:存储运算处理后的CPU的状态。 -***程序计数器(Program Counter, PC)***:存储下一条指令所在内存的地址。 +- ***程序计数器(Program Counter, PC)***:存储下一条指令所在内存的地址。 -***基址寄存器***:存储数据内存的起始地址。 +- ***基址寄存器***:存储数据内存的起始地址。 -***变址寄存器***:存储基址寄存器的相对地址。 +- ***变址寄存器***:存储基址寄存器的相对地址。 -***通用寄存器***:存储任意数据。 +- ***通用寄存器***:存储任意数据。 -***指令寄存器(Instruction Register, IR)***:存储指令,CPU取到的指令存放在处理器的一个寄存器中,这个寄存器就是指令寄存器。CPU内部使用,程序员无法通过程序对该寄存器进行读写操作。 +- ***指令寄存器(Instruction Register, IR)***:存储指令,CPU取到的指令存放在处理器的一个寄存器中,这个寄存器就是指令寄存器。CPU内部使用,程序员无法通过程序对该寄存器进行读写操作。 -***栈寄存器***:存储栈区域的起始地址。 +- ***栈寄存器***:存储栈区域的起始地址。 其中,程序计数器,累加寄存器,标志寄存器,指令寄存器和栈寄存器都只有一个,其他的寄存器一般有多个。 @@ -191,11 +191,11 @@ CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从 ## CPU 指令执行过程 -那么 CPU 是如何执行一条条的指令的呢? +那么CPU是如何执行一条条的指令的呢? 几乎所有的冯·诺伊曼型计算机的CPU,其工作都可以分为5个阶段:**取指令、指令译码、执行指令、访存取数、结果写回**。 -- 取指令阶段是将内存中的指令读取到 CPU 中寄存器的过程,程序寄存器用于存储下一条指令所在的地址 +- 取指令阶段是将内存中的指令读取到CPU中寄存器的过程,程序寄存器用于存储下一条指令所在的地址 - 指令译码阶段,在取指令完成后,立马进入指令译码阶段,在指令译码阶段,指令译码器按照预定的指令格式,对取回的指令进行拆分和解释,识别区分出不同的指令类别以及各种获取操作数的方法。 - 执行指令阶段,译码完成后,就需要执行这一条指令了,此阶段的任务是完成指令所规定的各种操作,具体实现指令的功能。 - 访问取数阶段,根据指令的需要,有可能需要从内存中提取数据,此阶段的任务是:根据指令地址码,得到操作数在主存中的地址,并从主存中读取该操作数用于运算。 @@ -230,13 +230,13 @@ CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从 -所有计算机都提供了允许其他模块(I/O、存储器)中断处理器正常处理过程的机制。中断最初是用于提高处理器效率的一种手段。例如,多数I/O设备都要远慢于处理器,处理器必须暂停并保持空闲,直到打印机完成工作。暂停的时间长度可能相当于成百上千哥不涉及存储器的指令周期,显然,这对于处理器的使用来说是非常浪费的。这种只有一个单独程序的情况,称为单道程序设计。在单道程序设计中处理器话费一定的运行时间进行计算,直到遇到一个I/O指令,这时它必须等到该I/O指令结束后才能继续执行。这种问题是可以避免的,就是存储器可以保存多个程序,在一个程序等待时通过切换去执行其他的程序,这种处理称为多道程序设计或多任务处理。它是现代操作系统的主要方案。多道程序设计的目的是为了让处理器和I/O设备(包括存储设备)同时报出忙状态,以实现最大的效率。 +所有计算机都提供了允许其他模块(I/O、存储器)中断处理器正常处理过程的机制。中断最初是用于提高处理器效率的一种手段。例如,多数I/O设备都要远慢于处理器,处理器必须暂停并保持空闲,直到打印机完成工作。暂停的时间长度可能相当于成百上千个不涉及存储器的指令周期,显然,这对于处理器的使用来说是非常浪费的。这种只有一个单独程序的情况,称为单道程序设计。在单道程序设计中处理器话费一定的运行时间进行计算,直到遇到一个I/O指令,这时它必须等到该I/O指令结束后才能继续执行。这种问题是可以避免的,就是存储器可以保存多个程序,在一个程序等待时通过切换去执行其他的程序,这种处理称为多道程序设计或多任务处理。它是现代操作系统的主要方案。多道程序设计的目的是为了让处理器和I/O设备(包括存储设备)同时保持忙状态,以实现最大的效率。 -利用中断功能,处理器可以在I/O操作的执行过程中去执行其他命令。在这期间,如果I/O操作已经完成,此时外部设备在做好服务的准备后,即它准备好从处理器接收更多的数据时,外部设备的I/O模块给处理器发送一个中断请求信号。这时处理器会做出相应,暂停当前程序的处理,转去处理服务于特定I/O设备的程序,这种程序被称为中断处理程序(interupt handler)。在对该设备的服务响应完成后,处理器恢复原来的执行。 +利用中断功能,处理器可以在I/O操作的执行过程中去执行其他命令。在这期间,如果I/O操作已经完成,此时外部设备在做好服务的准备后,即它准备好从处理器接收更多的数据时,外部设备的I/O模块给处理器发送一个中断请求信号。这时处理器会做出响应,暂停当前程序的处理,转去处理服务于特定I/O设备的程序,这种程序被称为中断处理程序(interupt handler)。在对该设备的服务响应完成后,处理器恢复原来的执行。 -从用户程序的角度来看,中断打断了正常执行的序列。中断处理完成后,再回复执行。因此,用户程序并不需要为中断添加任何特殊的代码,处理器和操作系统负责挂起用户程序,然后在同一个地方恢复执行。 +从用户程序的角度来看,中断打断了正常执行的序列。中断处理完成后,再恢复执行。因此,用户程序并不需要为中断添加任何特殊的代码,处理器和操作系统负责挂起用户程序,然后在同一个地方恢复执行。 为使用中断产生的情况,在指令周期中要增加一个中断阶段。在中断阶段,处理器检查是否有中断发生,即检查是否出现中断信号。若没有中断,处理器继续运行,并在取指周期取当前程序的下一条指令。若有中断,处理器挂起当前程序的执行,并执行一个中断处理程序。这个中断处理程序通常是操作系统的一部分,它确定中断的性质,并执行所需要的操作。 @@ -256,7 +256,7 @@ CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从 2. 处理器在响应中断前结束当前指令的执行。 3. 处理器对中断进行测试,确定存在未响应的中断,并给提交中断的设备发送确认信号,确认信号允许该设备取消它的中断信号。 4. 处理器需要准备把控制权转交给中断程序。首先,需要保存从中断点恢复当前程序所需要的信息,要求的最少信息包括程序状态字(PSW)和保存在程序计数器(PC)中的下一条要执行的指令地址,它们被压入系统控制栈。 -5. 处理器把相应此中断的中断处理程序入口地址装入程序计数器。每类中断可由一个中断处理程序,具体取决于计算机系统架构和操作系统的设计。如果有多个中断程序,这一信息可能已包含在最初的中断信号中,否则处理器必须给发中断的设备发送请求,以获取含有所需信息的响应。一旦装入程序计数器,处理器就继续执行下一个指令周期,该指令周期也从取指开始。由于取指是由程序计数器的内容决定的,因此控制权被转交给中断处理程序,该程序会引起以下操作: +5. 处理器把响应此中断的中断处理程序入口地址装入程序计数器。每类中断可由一个中断处理程序,具体取决于计算机系统架构和操作系统的设计。如果有多个中断程序,这一信息可能已包含在最初的中断信号中,否则处理器必须给发中断的设备发送请求,以获取含有所需信息的响应。一旦装入程序计数器,处理器就继续执行下一个指令周期,该指令周期也从取指开始。由于取指是由程序计数器的内容决定的,因此控制权被转交给中断处理程序,该程序会引起以下操作: 6. 在这一点,与被中断程序相关的程序计数器和PSW被保存到系统栈中,此外,还有一些其他信息被当做正在执行程序的状态的一部分。特别需要保存处理器寄存器的内容,因为中断处理程序可能会用到这些寄存器,因此所有这些值和任何其他状态信息都需要保存。 7. 中断处理程序现在可以开始处理中断,其中包括检查与I/O操作相关的状态信息或其他引起中断的事件,还可能包括给I/O设备发送附加命令或应答。 8. 中断处理结束后,被保存的寄存器值从栈中释放并恢复到寄存器中。 @@ -284,7 +284,7 @@ CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从 - 高速缓存,它多数由硬件控制。 -- 主存:这是存储器系统的主力。主存通常称为随机访问存储器(Random Access Memory,RAM),内存是计算机中主要的内部内部存储器系统。内存中的每个单元位置都有唯一的地址对应,而且大多数机器指令会访问一个或多个内存地址。内存通常是告诉的、容量较小的告诉缓存的扩展。 +- 主存:这是存储器系统的主力。主存通常称为随机访问存储器(Random Access Memory,RAM),内存是计算机中主要的内部内部存储器系统。内存中的每个单元位置都有唯一的地址对应,而且大多数机器指令会访问一个或多个内存地址。内存通常是高速的、容量较小的高速缓存的扩展。 - 磁盘:磁盘同RAM相比,成本降低了,但是随机访问数据时间也慢了。其低速的原因是因为磁盘是一种机械装置。一个磁盘中有一个或多个金属盘片,他们以一定的速度旋转,从边缘开始有一个机械臂悬横在盘面上,这类似于老式播放塑料唱片的唱片机。 @@ -294,9 +294,9 @@ CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从 有时,还有一些实际上不是磁盘的磁盘,比如固态硬盘(Solid State Disk,SSD)。固态硬盘并没有可以移动的部分,外形也不像唱片那样,并且数据是存储在存储器(闪存)中的。与磁盘唯一的相似之处就是它也存储了大量即使在电源关闭时也不会丢失的数据。 - 这里要说一下闪存(flash memory),闪存是一种基于硅芯片的存储介质,可以用电写入或擦除。在便捷式电子设备中,闪存通常作为存储媒介。闪存是数吗相机中的胶卷。是便捷式音乐播放器的磁盘。这仅仅是闪存用途的两项。闪存在速度上介于RAM和磁盘之间。另外,与磁盘存储器不同的是,如果闪存擦除次数过多,就被磨损了。 + 这里要说一下闪存(flash memory),闪存是一种基于硅芯片的存储介质,可以用电写入或擦除。在便捷式电子设备中,闪存通常作为存储媒介。闪存是数码相机中的胶卷。是便捷式音乐播放器的磁盘。这仅仅是闪存用途的两项。闪存在速度上介于RAM和磁盘之间。另外,与磁盘存储器不同的是,如果闪存擦除次数过多,就被磨损了。 - 那闪存和固态硬盘有什么区别?固态硬盘也是将数据存储在闪存中。在存储行业中使用的最简单的类比之一是闪存就像鸡蛋,而SSD硬盘就像煎蛋卷一样。煎蛋卷主要是由鸡蛋制作的,而SSD硬盘主要由闪存支制成的。 + 那闪存和固态硬盘有什么区别?固态硬盘也是将数据存储在闪存中。在存储行业中使用的最简单的类比之一是闪存就像鸡蛋,而SSD硬盘就像煎蛋卷一样。煎蛋卷主要是由鸡蛋制作的,而SSD硬盘主要由闪存制成的。 @@ -397,6 +397,10 @@ Android在Linux内核中增加了两个提升电源管理能力的新功能: + + +---- + - [下一篇:2.进程与线程](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/2.%E8%BF%9B%E7%A8%8B%E4%B8%8E%E7%BA%BF%E7%A8%8B.md) diff --git "a/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" index 83bfc7f2..3f5369e6 100644 --- "a/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" +++ "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" @@ -24,9 +24,9 @@ ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/cpu_process.png) -上图是一种进程管理方法。两个进程A和B存在与内存中的某些部分,给每个进程(包含程序、数据和上下文信息)分配了一块存储器区域,并且在由操作系统建立和维护的进程表中进行了记录。进程表包含记录每个进程的表项,表项内容包括指向包含进程的存储块地址的指针,还包括该进程的部分和全部上下文。执行上下文的其余部分存放在别处,可能和进程本身存在一起,通常还可能保存在内存中的一块独立区域。进程索引寄存器(process index register)包含当前正在控制处理器的进程在进程表中的索引。程序计数器(program counter)指向该进程中下一条待执行的指令。基址寄存器中保存该存储器区域的开始地址。 +上图是一种进程管理方法。两个进程A和B存在于内存中的某些部分,给每个进程(包含程序、数据和上下文信息)分配了一块存储器区域,并且在由操作系统建立和维护的进程表中进行了记录。进程表包含记录每个进程的表项,表项内容包括指向包含进程的存储块地址的指针,还包括该进程的部分和全部上下文。执行上下文的其余部分存放在别处,可能和进程本身存在一起,通常还可能保存在内存中的一块独立区域。进程索引寄存器(process index register)包含当前正在控制处理器的进程在进程表中的索引。程序计数器(program counter)指向该进程中下一条待执行的指令。基址寄存器中保存该存储器区域的开始地址。 -图中表示,进程索引寄存器表明进程B正在执行。以前执行的进程被临时中断,在A中断的同事,所有寄存器的内容被记录在其执行上下文环境中,以后操作系统就可以执行进程切换。恢复进程A的执行。进程切换过程中包括保存B的上下文和恢复A的上下文。在程序计数器中载入指向A的程序区域的值时,进程A自动恢复执行。 +图中表示,进程索引寄存器表明进程B正在执行。以前执行的进程被临时中断,在A中断的同时,所有寄存器的内容被记录在其执行上下文环境中,以后操作系统就可以执行进程切换。恢复进程A的执行。进程切换过程中包括保存B的上下文和恢复A的上下文。在程序计数器中载入指向A的程序区域的值时,进程A自动恢复执行。 在任何多道程序设计系统中,CPU由一个进程快速切换至另一个进程,使每个进程各运行几十或几百毫秒。严格来说,在某一个瞬间,CPU只能运行一个进程。但在1秒钟内,它可能运行多个进程,这样就产生并行的错觉。CPU在各进程之间来回切换,这种快速的切换称为多道程序设计。 @@ -68,8 +68,8 @@ - 就绪态:进程做好了准备,只要有机会就开始执行 - 运行态:进程正在执行 -- 堵塞/等待态:进程在某些事件发生前补鞥呢执行,如I/O操作完成 -- 新建态:刚刚创建的进程,操作系统还未把他加入可执行进程组,它通常是进程控制块已经创建但还未加载到内存中的新进程 +- 堵塞/等待态:进程在某些事件发生前不能执行,如I/O操作完成 +- 新建态:刚刚创建的进程,操作系统还未把它加入可执行进程组,它通常是进程控制块已经创建但还未加载到内存中的新进程 - 退出态:操作系统从可执行进程组中释放出的进程,要么它自身已停止,要么它因某种原因被取消 ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/process_state.png) @@ -82,11 +82,11 @@ ### 进程由哪几部分组成? -进程是程序的一次运行过程,它是由程序段、数据段和进程控制块 PCB 组成的一个实体,其中: +进程是程序的一次运行过程,它是由程序段、数据段和进程控制块PCB组成的一个实体,其中: - 程序段:对应程序的操作代码部分,用于描述进程所需要完成的功能。 - 数据段:对应程序执行时所需要的数据部分,包括数据,堆栈和工作区。 -- 进程控制块(Process Control Block, PCB) :描述进程的基本信息和运行状态,记录了进程运行时所需要的全部信息,它是进程存在的唯一标识,与进程一一对应。所谓的创建进程和撤销进程,都是指对 PCB 的操作。 +- 进程控制块(Process Control Block, PCB) :描述进程的基本信息和运行状态,记录了进程运行时所需要的全部信息,它是进程存在的唯一标识,与进程一一对应。所谓的创建进程和撤销进程,都是指对PCB的操作。 #### 进程控制块 @@ -101,7 +101,7 @@ - I/O状态信息:包括显式I/O请求、分配给进程的I/O设备和被进程使用的文件列表等 - 记账信息:包括处理器时间总和、使用的时钟数总和、时间限制、及账号等 -上述列表信息存放在一个被称为进程控制块(process control block)的数据结构中,控制块由操作系统创建和管理。系统通过 PCB 感知进程的存在,并对其进行有效管理和控制。系统创建一个新进程时,为它建立一个PCB;当进程结束时,系统又收回其PCB,该进程也随之消亡。 +上述列表信息存放在一个进程控制块(process control block)的数据结构中,控制块由操作系统创建和管理。系统通过PCB感知进程的存在,并对其进行有效管理和控制。系统创建一个新进程时,为它建立一个PCB;当进程结束时,系统又收回其PCB,该进程也随之消亡。 @@ -119,7 +119,7 @@ ## 进程切换 -操作系统为了执行进程间的切换,会维护着一张表格,这张表就是 进程表(process table)。每个进程占用一个进程表项。该表项包含了进程状态的重要信息,包括程序计数器、堆栈指针、内存分配状况、所打开文件的状态、账号和调度信息,以及其他在进程由运行态转换到就绪态或阻塞态时所必须保存的信息,从而保证该进程随后能再次启动,就像从未被中断过一样。 +操作系统为了执行进程间的切换,会维护着一张表格,这张表就是进程表(process table)。每个进程占用一个进程表项。该表项包含了进程状态的重要信息,包括程序计数器、堆栈指针、内存分配状况、所打开文件的状态、账号和调度信息,以及其他在进程由运行态转换到就绪态或阻塞态时所必须保存的信息,从而保证该进程随后能再次启动,就像从未被中断过一样。 **操作系统最底层的就是调度程序**,在它上面有许多进程。所有关于中断处理、启动进程和停止进程的具体细节都隐藏在调度程序中。事实上,调度程序只是一段非常小的程序。 @@ -131,7 +131,7 @@ - 时钟中断:操作系统确定当前正运行进程的执行时间是否已超过最大允许时间段(时间片,即进程中断前可以执行的最大时间段)。若超过,进程就切换到就绪态,并调入另一个进程。 - I/O中断:操作系统确定是否已发生I/O活动。若I/O活动是一个或多个进程正在等待的事件,则操作系统就把所处于堵塞态的进程转换为就绪态( 堵塞/挂起态进程转换为就绪/挂起态)。操作系统必须决定是继续执行当前处于运行态的进程,还是让具有高优先级的就绪态进程抢占这个进程。 -- 内存失效:处理器遇到一个引用不在内存中的字的虚存地址时,操作系统就必须从外村中把包含这一引用的内存块(页或段)调入内存。发出调入内存块的I/O请求后,内存失效进程将进入堵塞态。操作系统然后切换进程,恢复另一个进程的执行。期望的块调入内存后,该进程置为就绪态。 +- 内存失效:处理器遇到一个引用不在内存中的字的虚存地址时,操作系统就必须从外存中把包含这一引用的内存块(页或段)调入内存。发出调入内存块的I/O请求后,内存失效进程将进入堵塞态。操作系统然后切换进程,恢复另一个进程的执行。在等该期望的内存块调入内存后,该进程置为就绪态。 对于陷阱(trap),操作系统则确定错误或异常条件是否致命。致命时,当前正运行进程置为退出态,并切换进程。不致命时,操作系统的动作将取决于错误的性质和操作系统的设计,操作系统可能会尝试恢复程序,或简单的通知用户。操作系统可能会切换进程或继续当前运行的进程。 @@ -175,7 +175,7 @@ - 信号 - 基本原来是:两个或多个进程可以通过简单的信号进行合作,可以强迫一个进程在某个位置停止,直到它接收到一个特定的信号。 + 两个或多个进程可以通过简单的信号进行合作,可以强迫一个进程在某个位置停止,直到它接收到一个特定的信号。 - 共享存储系统 @@ -349,7 +349,7 @@ zygote进程对应的具体程序是app_process,该程序存在于system/bin #### System Server进程 -System Server进程是由zygote进程fork而来,System Server是zygote孵化的第一个Dalvik进程,SystemServer仅仅是该进程的别名,而该进程具体对应的程序依然是app_process,因为System是从app_process中孵化出来的。System Server负责启动和管理整个Java framework,SystemServer进程在Android的运行环境中扮演了“神经中枢”的作用,APK应用中能够直接交互的大部分系统服务都在该进程中运行,常见的有WindowManagerServer(WmS)、ActivityManagerSystemService(AmS)、PackageManagerServer(PmS)等,这些系统服务都是以一个线程的方式存在于SystemServer进程中。SystemServer的main()函数会首先创建一个ServerThread对象,该对象是一个线程,然后直接运行该线程。而在ServerThread的run()方法内部真正启动各种服务线程,都有: +System Server进程是由zygote进程fork而来,System Server是zygote孵化的第一个Dalvik进程,SystemServer仅仅是该进程的别名,而该进程具体对应的程序依然是app_process,因为System是从app_process中孵化出来的。System Server负责启动和管理整个Java framework,SystemServer进程在Android的运行环境中扮演了“神经中枢”的作用,APK应用中能够直接交互的大部分系统服务都在该进程中运行,常见的有WindowManagerServer(WmS)、ActivityManagerService(AmS)、PackageManagerServer(PmS)等,这些系统服务都是以一个线程的方式存在于SystemServer进程中。SystemServer的main()函数会首先创建一个ServerThread对象,该对象是一个线程,然后直接运行该线程。而在ServerThread的run()方法内部真正启动各种服务线程,都有: - EntropyService:提供伪随机数 - PowerManagerService:电源管理服务 @@ -384,7 +384,7 @@ SystemServer中创建了一个Socket客户端,并有AmS负责管理该客户 从系统架构的角度来看,先创建一个zygote并加载共享类的资源,然后通过该zygote去孵化新的Dalvik进程,该架构的特点有两个: -- 每一个进程都是一个Dalvik虚拟机,而Dalvik虚拟机是一个类似于Java虚拟机的程序,并且从开发的过程来看,与标准的Java程序开发基本一致。因此对于程序员来讲,必须要学习新的语言,并可以使用Java程序在过去几十年中已经成熟的各种类库资源。 +- 每一个进程都是一个Dalvik虚拟机,而Dalvik虚拟机是一个类似于Java虚拟机的程序,并且从开发的过程来看,与标准的Java程序开发基本一致。因此对于程序员来讲,不须要学习新的语言,并可以使用Java程序在过去几十年中已经成熟的各种类库资源。 - zygote进程预先会装载共享类和共享资源,这些类及资源实际上就是SDK中定义的大部分类和资源,因此,当通过zygote孵化出新的进程后,新的APK进程只需要去装载APK自身包含的类和资源即可,这就有效的解决了多个APK共享Framework资源的问题。 @@ -401,7 +401,7 @@ Android使用的进程有些不同。活动管理器是Android负责正在运行 ### 启动进程 -为了启动新进程,活动管理器需要与zygote通信。活动管理器首先开始,它创建一个与zygote相连的专用接口,通过接口发送一条指令,表示它需要启动一个进程。这条指令主要描述需要创建的沙箱、新进程运行所需要的UID以及需要遵守的安全性制约。zygote需要作为根来运行:创建新进程时,它合理配置运行所需的UID,最终下放权限,将进程改为该UID。 +为了启动新进程,活动管理器需要与zygote通信。活动管理器首先创建一个与zygote相连的专用接口,通过接口发送一条指令,表示它需要启动一个进程。这条指令主要描述需要创建的沙箱、新进程运行所需要的UID以及需要遵守的安全性制约。zygote需要作为根来运行:创建新进程时,它合理配置运行所需的UID,最终下放权限,将进程改为该UID。 ![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_start_process.png?raw=true) @@ -409,8 +409,8 @@ Android使用的进程有些不同。活动管理器是Android负责正在运行 1. 某个现有进程(如应用程序启动器)调用活动管理器,发出意图,描述它想要启动的新活动。 2. 活动管理器要求封装管理器将这个意图解析为一个明确的组件。 -3. 活动管理器判断这个应用程序的进程并未正在运行,然后向zygote请求一个具有合适UID的新锦成。 -4. zygote进行一次fork指令,克隆自己来创造一个新进程,下方权限并配置新进程的UID和沙箱,初始化该进程的Dalvik,使得Java runtime开始完全执行。例如,它需要在fork后启动垃圾收集等线程。 +3. 活动管理器判断这个应用程序的进程并未正在运行,然后向zygote请求一个具有合适UID的新进程。 +4. zygote进行一次fork指令,克隆自己来创造一个新进程,下放权限并配置新进程的UID和沙箱,初始化该进程的Dalvik,使得Java runtime开始完全执行。例如,它需要在fork后启动垃圾收集等线程。 5. 新进程如今是一个zygote的克隆,并运行着完全配置好的Java环境。它回调活动管理器,询问后者“我该做什么”。 6. 活动管理器返回即将启动的应用程序的完整信息,如源码位置等。 7. 新进程读取应用程序的源码,开始运行。 @@ -421,23 +421,6 @@ Android使用的进程有些不同。活动管理器是Android负责正在运行 - - -### 进程生命周期 - -活动管理器也负责判断何时进程不再被需要。活动管理器记录一个进程中运行的所有活动、接收器、服务以及内容提供其,据此可判断该进程的重要程度。 - -Android内核中的内存溢出强制结束指令是使用一个进程的oom_adj进行严格排序,决定哪个进程需要优先强制结束。活动管理器负责基于每个进程的状态,通过将其归类于几个主要用途,从而合理设定器oom_adj。/proc//oom_adj: - -- 取值是-17到+15,取值越高,越容易被干掉。如果是-17,则表示不能被kill -- 该设置参数的存在是为了和旧版本的内核兼容 - -![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_process_adj.png?raw=true) - -让RAM内存不足时,系统已经完成了进程的配置,使得内存溢出强制结束命令优先中止缓存(cache)类型的进程,尝试重新取得足够的所需内存,随后中止界面(home)类别、服务(service)类别,以此类推。在同一个oom_adj水平中,它将优先中止内存占用较大的进程。 - - - ### Android系统启动的核心流程如下: @@ -452,9 +435,9 @@ Android内核中的内存溢出强制结束指令是使用一个进程的oom_adj 3. Linux内核启动:当内核启动时,设置缓存、被保护存储器、计划列表、加载驱动。当其完成系统设置时,会先在系统文件中寻找init.rc文件,并启动init进程。 4. init进程启动:初始化和启动属性服务,init进程会孵化出eadbd、logd、等用户守护进程,还会启动ServiceManager(binder服务管家)、botanic(开机动画)等服务,并且启动Zygote进程。 5. Zygote进程启动:zygote进程是Android系统的第一个Java进程(即虚拟机进程),它是所有Java进程的父进程。它会创建JVM并为其注册JNI方法,创建服务器端Socket,启动SystemServer进程。并且,zygote进程在启动的时候会创建DVM或者ART。因此通过从zygote进程fork创建的应用程序进程和systemserver进程都可以在内部获取一个DVM或者ART的实例副本。它还会提前加载类preloadClasses和提前加载资源preloadResouces。 -6. SystemServer进程启动:System Server是zygote孵化的第一个进程,它会启动Binder线程池和SystemServiceManager,并且启动各种系统服务,包括ActivityManager、WindowManager、PackageManager、PowerManager等服务。 +6. SystemServer进程启动:System Server是zygote孵化的第一个进程,它会启动Binder线程池和SystemServiceManager,并且启动各种系统服务,包括ActivityManagerService、WindowManagerService、PackageManagerService、PowerManagerService等服务。 7. Media Server进程,是由init进程fork而来,负责启动和管理整个C++ framework,包括AudioFlinger,Camera Service等服务。 -8. Launcher启动:是zygote孵化的第一个App进程,被SystemServer进程启动的AMS会启动Launcher,Launcher启动后会将已安装应用的快捷图标显示到系统桌面上。zygote还会创建Broweer、Phone、Email等App进程,每个App至少运行在一个进程上。 +8. Launcher启动:是zygote孵化的第一个App进程,被SystemServer进程启动的AmS会启动Launcher,Launcher启动后会将已安装应用的快捷图标显示到系统桌面上。zygote还会创建Broweer、Phone、Email等App进程,每个App至少运行在一个进程上。 @@ -494,9 +477,13 @@ Android内核中的内存溢出强制结束指令是使用一个进程的oom_adj +--- + + + - [上一篇:1.操作系统简介](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/1.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%AE%80%E4%BB%8B.md) -- [下一篇:2.进程与线程](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/2.%E8%BF%9B%E7%A8%8B%E4%B8%8E%E7%BA%BF%E7%A8%8B.md) +- [下一篇:3.内存管理](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/3.%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86.md) --- diff --git "a/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" "b/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" index 2490734b..fa5e3048 100644 --- "a/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" +++ "b/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" @@ -19,7 +19,7 @@ -进程对应的内存空间中所包含的5种不同的数据区: +进程对应的内存空间中所包含的6种不同的数据区: - 代码段(code segment):用来存放程序运行代码的一块内存空间。此空间大小在代码运行前就已经确定。内存空间一般属于只读,某些架构的代码也允许可写。 @@ -28,7 +28,7 @@ - BSS段(bss segment):存储未初始化的全局变量和未初始化的static变量。bss段中数据的生存期随进程持续性。bss段中的数据一般默认为0.这里很奇怪,一般的书上都会说全局变量和静态变量是会自动初始化的,怎么突然来了个未初始化的变量?其实变量的初始化可以分为显式初始化和隐式初始化,全局变量和静态变量如果程序员自己不初始化的话也会被初始化,那就是不管什么类型都初始化为默认值,这种没有显示初始化的就是这里所说的未初始化。例如整数型的全局变量未初始化的默认隐式初始化的值为0,都是0就没必要把每个0都存储起来,从而节省磁盘空间,这是BSS的主要作用)。BSS的全称是Block Started by Symbol,它属于静态内存分配。BSS节不包含任何数据,只是简单的维护开始和结束的地址,即总大小,以便内存能在运行时分配并被有效的清零。BSS节在应用程序的二进制映像文件中并不存在,既不占用磁盘空间,而只在运行的时候占用内存空间,所以如果全局变量和静态变量未初始化那么其可执行文件要小很多。 - rodata段(read only data):常量区,存放只读数据。比如程序中定义为const的全局变量,“Hello Word”的字符串常量。有些系统中rodata段是多个进程共享的,目的是为了提高空间利用率。在有的嵌入式系统中,rodata放在ROM中,运行时直接读取,不须加载到RAM内存中。所以在嵌入式开发中,常将已知的常量系数,表格数据等加以const关键字。存放在ROM中,避免占用RAM空间。 -- 堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc、realloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减) +- 堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc、realloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减) - 栈(stack):栈是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。栈的生存期随代码块持续性,代码块运行就给你分配空间,代码块结束就自动回收空间。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。 @@ -38,11 +38,11 @@ ## 内存的演变 -在早些的操作系统中,并没有引入内存抽象的概念。程序直接访问和操作的都是物理内存,内存的管理也非常简单,除去操作系统所用的内存之外,全部给用户程序使用,想怎么折腾都行,只要别超出最大的容量。这种内存操作方式使得操作系统中存在多进程变得完全不可能,比如MS-DOS,你必须执行完一条指令后才能接着执行下一条。如果是多进程的话,由于直接操作物理内存地址,当一个进程给内存地址1000赋值后,另一个进程也同样给内存地址赋值,那么第二个进程对内存的赋值会覆盖第一个进程所赋的值,这回造成两条进程同时崩溃。随着计算机技术发展,要求操作系统支持多进程的需求,所谓多进程,并不需要同时运行这些进程,只要它们都处于 ready 状态,操作系统快速地在它们之间切换,就能达到同时运行的假象。每个进程都需要内存,Context Switch 时,之前内存里的内容怎么办?简单粗暴的方式就是先 dump 到磁盘上,然后再从磁盘上 restore 之前 dump 的内容(如果有的话),但效果并不好,太慢了!那怎么才能不慢呢?把进程对应的内存依旧留在物理内存中,需要的时候就切换到特定的区域。这就涉及到了内存的保护机制,毕竟进程之间可以随意读取、写入内容就乱套了,非常不安全。因此操作系统需要对物理内存做一层抽象,也就是「地址空间」(Address Space),一个进程的地址空间包含了该进程所有相关内存,比如 code / stack / heap。一个 16 KB 的地址空间可能长这样: +在早些的操作系统中,并没有引入内存抽象的概念。程序直接访问和操作的都是物理内存,内存的管理也非常简单,除去操作系统所用的内存之外,全部给用户程序使用,想怎么折腾都行,只要别超出最大的容量。这种内存操作方式使得操作系统中存在多进程变得完全不可能,比如MS-DOS,你必须执行完一条指令后才能接着执行下一条。如果是多进程的话,由于直接操作物理内存地址,当一个进程给内存地址1000赋值后,另一个进程也同样给内存地址赋值,那么第二个进程对内存的赋值会覆盖第一个进程所赋的值,这回造成两条进程同时崩溃。随着计算机技术发展,要求操作系统支持多进程的需求,所谓多进程,并不需要同时运行这些进程,只要它们都处于ready 状态,操作系统快速地在它们之间切换,就能达到同时运行的假象。每个进程都需要内存,Context Switch时,之前内存里的内容怎么办?简单粗暴的方式就是先dump到磁盘上,然后再从磁盘上restore之前dump的内容(如果有的话),但效果并不好,太慢了!那怎么才能不慢呢?把进程对应的内存依旧留在物理内存中,需要的时候就切换到特定的区域。这就涉及到了内存的保护机制,毕竟进程之间可以随意读取、写入内容就乱套了,非常不安全。因此操作系统需要对物理内存做一层抽象,也就是「地址空间」(Address Space),一个进程的地址空间包含了该进程所有相关内存,比如code/stack/heap。一个16KB的地址空间可能长这样: ![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/memory_1.jpg?raw=true) -Stack 和 Heap 中间有一块 free space,即使没有用,也被占着,那如何才能解放这块区域呢,那就是虚拟内存。 +Stack和Heap中间有一块free space,即使没有用,也被占着,那如何才能解放这块区域呢,那就是虚拟内存。 Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自互不干涉的进程地址空间。该空间是块大小为4G的线性虚拟空间,用户所看到和接触到的都是该虚拟地址,无法看到实际的物理内存地址。利用这种虚拟地址不但能起到保护操作系统的效果(用户不能直接访问物理内存),而且更重要的是,用户程序可使用比实际物理内存更大的地址空间。 @@ -138,7 +138,7 @@ Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自 分页和分段的两个特点: -- 进程中的所有内存访问的都是逻辑地址,这些逻辑地址会在运行时动态地址转换为物理地址。这意味着一个进程可被换入或换出内存,因此进程可在执行过程中的不同时刻占据内存中的不同区域。 +- 进程中的所有内存访问的都是逻辑地址,这些逻辑地址会在运行时把动态地址转换为物理地址。这意味着一个进程可被换入或换出内存,因此进程可在执行过程中的不同时刻占据内存中的不同区域。 - 一个进程可划分为许多块(页和段),在执行过程中,这些块不需要连续的位于内存中。动态运行时地址转换和页表或段表的使用使得这一点成为可能。 由于上面这两个特点的存在,那么在一个进程的执行过程中,该进程不需要所有页或段都在内存中。如果内存中保存有待取的下一条指令所在块(段或页)及待访问的下一个数据单元所在块,那么执行至少可以暂时继续下去。我们用术语“块”来表示页或段。处理器在需要访问一个不在内存中的逻辑地址时,会产生一个中断,这表明出现了内存访问故障。操作系统会把被中断的进程置于堵塞态。要继续执行这个进程,操作系统必须把包含引发访问故障的逻辑地址的进程块读入内存。为此操作系统产生一个磁盘I/O读请求。产生I/O请求后,在执行磁盘I/O期间,操作系统可以调度另一个进程运行。在需要的块读入内存后,产生一个I/O中断,控制权交回给操作系统,而操作系统则把由于缺少该块而被堵塞的进程置为就绪态。这样的话就会有两种提高系统利用率的方法: @@ -162,14 +162,14 @@ Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自 要有效的使用处理器和I/O设备,就需要在内存中保留尽可能多的进程。此外,还需要解除程序在开发时对程序使用内存大小的限制。解决这两个问题的途径就是虚拟内存技术。采用虚拟内存技术时,所有的地址访问都是逻辑访问,并在运行时转换为实地址。 -虚拟内存机制使得期望运行大于物理内存的程序称为可能。其方法是将程序放在磁盘上,而将主存作为一种缓存,用来保存最频繁使用的部分程序。这种机制需要快速的映像内存地址,以便把程序生成的地址转换为有关字节在RAM中的物理地址。这种映像由CPU中的一个称为存储器管理单元(Memory Management Unit,MMU)的部件来完成。 +虚拟内存机制使得期望运行大于物理内存的程序成为可能。其方法是将程序放在磁盘上,而将主存作为一种缓存,用来保存最频繁使用的部分程序。这种机制需要快速的映像内存地址,以便把程序生成的地址转换为有关字节在RAM中的物理地址。这种映像由CPU中的一个称为存储器管理单元(Memory Management Unit,MMU)的部件来完成。 **虚拟内存**的**基本思想**是:每个程序都拥有自己的地址空间,这个空间被分割成多个块,每个块被成为一页或页面. **程序运行时,并不是所有页都在物理内存中**: - 当程序引用一部分在物理内存的地址空间时,由硬件直接执行必要的映射; -- 当程序引用一部分不在物理内存的地址空间时,有操作系统将缺失的页装入物理内存,并重新运行 +- 当程序引用一部分不在物理内存的地址空间时,由操作系统将缺失的页装入物理内存,并重新运行 虚拟内存基于分段和分页这两种基本技术或基于这两种技术中的一种。虚拟内存机制允许程序以逻辑方式访问存储器,而不考虑物理内存上可用的空间数量。虚存的构想是为了满足有多个用户作业同时驻留在内存中的要求,因此在一个进程被写出到副主存储器中且后续进程被读入时,连续的进程执行之间将不会脱节。进程大小不同时,若处理器在很多进程间切换,则很难把它们紧密的压入内存,因此人们引入了分页系统。在分页系统中,进程由许多固定大小的块组成。这些块成为页。程序通过虚地址(virtual address)访问,虚地址由页号和页中的偏移量组成。进程的每页都可置于内存中的任何地方,分页系统提供了程序中使用的虚地址和内存中的实地址(real address)或物理地址之间的动态映射。 @@ -179,9 +179,9 @@ Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自 1. CPU中包含**MMU内存管理单元**,用于管理虚拟地址空间到物理内存地址的映射. 2. 假设物理内存地址大小为32k,每4k为一个页框.虚拟地址空间分页,每个页面大小等于一个页框 -3. 当程序想要访问一个虚拟地址x, -4. 指令将x送到MMU, -5. MMU根据x的虚拟地址,判断其对应的页面是否在物理内存中: +3. 当程序想要访问一个虚拟地址x +4. 指令将x送到MMU +5. MMU根据x的虚拟地址,判断其对应的页面是否在物理内存中 6. 若在,MMU将x转化为物理内存地址y 7. 若不在,则进行缺页中断,操作系统在物理内存中找到一个使用较少的页面回收掉,将需要访问的页面读到被回收的页面处,再将x转化为物理内存地址访问 @@ -197,7 +197,7 @@ Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自 - 请求分页 - 只有当访问到某页中的一个单元时才将改页取入内存。若内存管理的其他策略比较合适,将发生下述情况:当一个进程首次启动时,会在一段时间出现大量的缺页中断,取入越来越多的页后,局部性原理表明大多数将来访问的页都是最近去读的页。因此在一段时间后错误会逐渐减少,缺页中断的数量会降低到很低。 + 只有当访问到某页中的一个单元时才将该页取入内存。若内存管理的其他策略比较合适,将发生下述情况:当一个进程首次启动时,会在一段时间出现大量的缺页中断,取入越来越多的页后,局部性原理表明大多数将来访问的页都是最近去读的页。因此在一段时间后错误会逐渐减少,缺页中断的数量会降低到很低。 - 预先分页 @@ -233,7 +233,7 @@ Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自 #### 6.加载控制 -加载控制会影响到驻留内存中的进程数量,这称为系统并发度。加载控制策略在有效的内存管理中非常重要。如果某一时刻驻留的进程太少,那么所有进程都处于堵塞态的概率就较大,因为会有许多时间话费在交换上。另一方面,如果驻留的进程太多,平均每个进程的驻留集大小将会不够用,此时会频繁发生缺页中断,从而导致系统抖动。 +加载控制会影响到驻留内存中的进程数量,这称为系统并发度。加载控制策略在有效的内存管理中非常重要。如果某一时刻驻留的进程太少,那么所有进程都处于堵塞态的概率就较大,因为会有许多时间花费在交换上。另一方面,如果驻留的进程太多,平均每个进程的驻留集大小将会不够用,此时会频繁发生缺页中断,从而导致系统抖动。 @@ -244,14 +244,21 @@ Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自 Android包含了标准Linux内核中内存管理设施的许多扩展,具体如下: - ASHMem:这个功能提供匿名共享内存,它将内存抽象为文件描述符。文件描述符可以传递给另一个进程以共享内存。 + - Pmen:这个功能分配虚拟内存,使的它在物理上是连续的,因此对于那些不支持虚拟内存的设备非常实用。 -- Low Memory Killer:andorid基于oomKiller原理所扩展的一个多层次oomKiller。在Android中运行了一个OOM进程,该进程启动时会首先向Linux内核中把自己注册为一个OOM Killer,即当Linux内核的内存管理模块检测到系统内存低的时候会通知已经注册的OOM进程,然后这些OOMkille会选择性杀掉进程,到oom的时候,系统可能已经不太稳定,而LowMemoryKiller是一种根据内存阈值级别触发的内存回收的机制,在系统可用内存较低时,就会选择性杀死进程的策略,相对OOMKiller,更加灵活。主存耗尽时,使用大量内存的应用不是让步对内存的使用就是被终结。这个功能可让系统通知应用释放内存,如果应用不配合,则终结应用。 +- Low Memory Killer:andorid基于OOM Killer原理所扩展的一个多层次OOM Killer。在Android中运行了一个OOM进程,该进程启动时会首先向Linux内核中把自己注册为一个OOM Killer,即当Linux内核的内存管理模块检测到系统内存低的时候会通知已经注册的OOM进程,然后这些OOM kille会选择性杀掉进程,到OOM的时候,系统可能已经不太稳定,而LowMemoryKiller是一种根据内存阈值级别触发的内存回收的机制,在系统可用内存较低时,就会选择性杀死进程的策略,相对OOM Killer,更加灵活。主存耗尽时,使用大量内存的应用不是让步对内存的使用就是被终结。这个功能可让系统通知应用释放内存,如果应用不配合,则终结应用。这些都是由活动管理器来负责判断何时进程不再被需要。活动管理器记录一个进程中运行的所有活动、接收器、服务以及内容提供者,据此可判断该进程的重要程度。 + + Android内核中的内存溢出强制结束指令是使用一个进程的oom_adj进行严格排序,决定哪个进程需要优先强制结束。活动管理器负责基于每个进程的状态,通过将其归类于几个主要用途,从而合理设定其oom_adj。/proc//oom_adj: + - 取值是-17到+15,取值越高,越容易被干掉。如果是-17,则表示不能被kill + - 该设置参数的存在是为了和旧版本的内核兼容 -Android的底层Linux未采用磁盘虚拟内存机制,程序只能使用屋里内存作为最大内存,所以AmS中采用了自动杀死优先级较低进程的方法以达到释放内存的目的。 + ![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_process_adj.png?raw=true) + 让RAM内存不足时,系统已经完成了进程的配置,使得内存溢出强制结束命令优先中止缓存(cache)类型的进程,尝试重新取得足够的所需内存,随后中止界面(home)类别、服务(service)类别,以此类推。在同一个oom_adj水平中,它将优先中止内存占用较大的进程。 + Android的底层Linux未采用磁盘虚拟内存机制,程序只能使用屋里内存作为最大内存,所以AmS中采用了自动杀死优先级较低进程的方法以达到释放内存的目的。 @@ -261,6 +268,8 @@ Android的底层Linux未采用磁盘虚拟内存机制,程序只能使用屋 +--- + - [上一篇:2.进程和线程](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/2.%E8%BF%9B%E7%A8%8B%E4%B8%8E%E7%BA%BF%E7%A8%8B.md) diff --git "a/OperatingSystem/4.\350\260\203\345\272\246.md" "b/OperatingSystem/4.\350\260\203\345\272\246.md" index 568d6cf7..97635ae5 100644 --- "a/OperatingSystem/4.\350\260\203\345\272\246.md" +++ "b/OperatingSystem/4.\350\260\203\345\272\246.md" @@ -2,7 +2,7 @@ -在多道程序设计系统中,内存中有多个进程,每个进程要么正在处理器上运行,要么正在等待某些事件的发生,比如I/O完成。处理器调度的目的是:以满足系统目标(如响应时间、吞吐率、处理器效率)的方式,把进程分配到一个或多个处理器上执行。处理器(或处理器组)通过执行某个进程而保持忙状态,而此时其他进程处理堵塞状态。典型的调度有四种: +在多道程序设计系统中,内存中有多个进程,每个进程要么正在处理器上运行,要么正在等待某些事件的发生,比如I/O完成。处理器调度的目的是:以满足系统目标(如响应时间、吞吐率、处理器效率)的方式,把进程分配到一个或多个处理器上执行。处理器(或处理器组)通过执行某个进程而保持忙状态,而此时其他进程处于堵塞状态。典型的调度有四种: ### 长程调度 @@ -41,13 +41,13 @@ 多处理器系统分为以下几类: -- 松耦合、分布式多处理器、集群:又一系列相对自治的系统组成,每个处理器都有自身的内存和I/O通道。 +- 松耦合、分布式多处理器、集群:由一系列相对自治的系统组成,每个处理器都有自身的内存和I/O通道。 - 专用处理器:I/O处理器是一个典型的例子。此时,有一个通用的主处理器,专用处理器由主处理器控制,并为主处理器提供服务。 - 紧耦合多处理器:由一些列共享同一个内存并受操作系统完全控制的处理器组成。 -多处理器中的调度设计三个相互关联的问题: +多处理器中的调度涉及三个相互关联的问题: - 把进程分配到处理器 @@ -71,17 +71,19 @@ 在多处理器线程调度和处理器分配的各种方案中,有四个比较突出的方法: -- 负载分配:进程不分配到某个特定的从处理器。系统维护一个就绪线程的全局队列,每个处理器只要空闲就从队列中选择一个线程。 +- 负载分配:进程不分配到某个特定的处理器。系统维护一个就绪线程的全局队列,每个处理器只要空闲就从队列中选择一个线程。 - 组调度:一组相关的的线程基于一对一的原则,同时调度到一组处理器上运行。 - 专用处理器分配:这种方法与负载分配方法正好相反,它通过把线程指定到处理器来定义隐式的调度。每个程序在其执行过程中,都分配给一组处理器,处理器的数量与程序中线程的数量相等。程序终止时,处理器返回总处理器池,以便分配给另一个程序。 - 动态调度:在执行期间,进程中线程的数据可以改变,允许动态地改变进程中的线程数量,这就使得操作系统可以通过调整负载情况来提高利用率。 +--- + - [上一篇:3.内存管理](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/3.%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86.md) -- [下一篇:5.I/O](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/5.I:O.md) +- [下一篇:5.I/O](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/5.I:O.md) --- diff --git a/OperatingSystem/5.I:O.md b/OperatingSystem/5.I:O.md index 29367863..35535833 100644 --- a/OperatingSystem/5.I:O.md +++ b/OperatingSystem/5.I:O.md @@ -18,7 +18,7 @@ DMA技术工作流程如下: - 如果处理器想读或写一块数据时,它通过想DMA模块发送以下信息来给DMA模块发出一条命令: + 如果处理器想读或写一块数据时,它通过向DMA模块发送以下信息来给DMA模块发出一条命令: - 请求读操作或写操作的信号,通过在处理器和DMA模块之间使用读写控制线发送。 - 相关I/O设备地址,通过数据线发送。 @@ -33,7 +33,7 @@ -高速缓冲存储器(cache memory)通常指比内存小且比内存块的存储器,它位于内存和处理器之间。这种高速缓冲存储器利用局部性原理来减少平均存储器的存取时间。同样的原理也适用于磁盘存储器。磁盘高速缓存是内存中为磁盘扇区设置的一个缓冲区,它包含有磁盘中某些扇区的副本。出现对某一特定扇区的I/O请求时,首先会进行检测,以确定该扇区是否在磁盘的告诉缓存中。若在则该请求可通过这个告诉缓存来满足。若不在,则把被请求的扇区从磁盘读到磁盘高速缓存中。 +高速缓冲存储器(cache memory)通常指比内存小且比内存块的存储器,它位于内存和处理器之间。这种高速缓冲存储器利用局部性原理来减少平均存储器的存取时间。同样的原理也适用于磁盘存储器。磁盘高速缓存是内存中为磁盘扇区设置的一个缓冲区,它包含有磁盘中某些扇区的副本。出现对某一特定扇区的I/O请求时,首先会进行检测,以确定该扇区是否在磁盘的高速缓存中。若在则该请求可通过这个高速缓存来满足。若不在,则把被请求的扇区从磁盘读到磁盘高速缓存中。 @@ -46,6 +46,10 @@ +--- + + + - [上一篇:4.调度](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/4.%E8%B0%83%E5%BA%A6.md) - [下一篇:6.文件管理](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/6.%E6%96%87%E4%BB%B6%E7%AE%A1%E7%90%86.md) diff --git "a/OperatingSystem/6.\346\226\207\344\273\266\347\256\241\347\220\206.md" "b/OperatingSystem/6.\346\226\207\344\273\266\347\256\241\347\220\206.md" index 3621dd4d..4940fb5d 100644 --- "a/OperatingSystem/6.\346\226\207\344\273\266\347\256\241\347\220\206.md" +++ "b/OperatingSystem/6.\346\226\207\344\273\266\347\256\241\347\220\206.md" @@ -54,6 +54,8 @@ Android文件系统目录的顶层部分: +--- + - [上一篇:5.I/O](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/5.I:O.md) diff --git "a/OperatingSystem/7.\345\265\214\345\205\245\345\274\217\347\263\273\347\273\237.md" "b/OperatingSystem/7.\345\265\214\345\205\245\345\274\217\347\263\273\347\273\237.md" index e37630e3..ac818dfb 100644 --- "a/OperatingSystem/7.\345\265\214\345\205\245\345\274\217\347\263\273\347\273\237.md" +++ "b/OperatingSystem/7.\345\265\214\345\205\245\345\274\217\347\263\273\347\273\237.md" @@ -18,7 +18,7 @@ Android是基于Linux内核的一个嵌入式系统,因此我们可以认为An - +--- - [上一篇:6.文件管理](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/6.%E6%96%87%E4%BB%B6%E7%AE%A1%E7%90%86.md) - [下一篇:8.虚拟机](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/8.%E8%99%9A%E6%8B%9F%E6%9C%BA.md) diff --git "a/OperatingSystem/8.\350\231\232\346\213\237\346\234\272.md" "b/OperatingSystem/8.\350\231\232\346\213\237\346\234\272.md" index 47f61216..ea7fd949 100644 --- "a/OperatingSystem/8.\350\231\232\346\213\237\346\234\272.md" +++ "b/OperatingSystem/8.\350\231\232\346\213\237\346\234\272.md" @@ -16,7 +16,7 @@ 尽管Java虚拟机(JVM)用“虚拟机”作为其名称的一部分,但其实现和用途与我们前面所讲的模型不同。虚拟机管理程序支持在主机上运行一个或多个虚拟机。这些虚拟机独立的处理工作负载,支持操作系统和应用,且在它们自身看来,访问一系列提供计算、存储和输入/输出的资源。Java虚拟机的目的是,无须更改任何Java代码就可在任意硬件平台的任意操作系统上,提供运行时空间。两种模型的目的都是通过使用某种程度的抽象化来实现平台无关性。 -JVM可描述为一个抽象的计算设备,它包含指令集、一个PC(程序计数器)寄存器、一个用来保存变量和结果的栈、一个保存运行时数据和垃圾手机的堆、一个存储代码和常量的方法区。 +JVM可描述为一个抽象的计算设备,它包含指令集、一个PC(程序计数器)寄存器、一个用来保存变量和结果的栈、一个保存运行时数据和垃圾收集的堆、一个存储代码和常量的方法区。 JVM支持多个线程,每个线程都有自己的寄存器和堆栈区,且所有线程共享栈和方法区。 @@ -81,9 +81,10 @@ DVM运行Java语言的应用和代码。标准的Java编译器将源代码(写 以上命令首先将jar包push到/data/app目录下,因为该目录一般用于存放应用程序,接着使用adb shell执行dalvikvm程序。dalvikvm的执行语法是: dalvikvm -cp 类路径 类名,从这里可以感觉到,dalvikvm的作用就像在pc上执行java程序一样。 - +--- - [上一篇:7.嵌入式系统](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/7.%E5%B5%8C%E5%85%A5%E5%BC%8F%E7%B3%BB%E7%BB%9F.md) +- [下一篇:Android内核](https://github.com/CharonChui/AndroidNote/tree/master/OperatingSystem/AndroidKernal) --- diff --git "a/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" "b/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" index 0a6b51a4..68b17d24 100644 --- "a/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" +++ "b/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" @@ -11,9 +11,9 @@ Binder,英文的意思是别针、回形针。我们经常用别针把两张 无论是Android系统,还是各种Linux衍生系统,各个组件、模块往往运行在各种不同的进程和线程内,这里就必然涉及进程/线程之间的通信。对于IPC(Inter-Process Communication, 进程间通信),Linux目前有一下这些IPC机制: - 管道:在创建时分配一个page大小的内存,缓存区大小比较有限; -- 消息队列:信息复制两次,额外的CPU消耗;不合适频繁或信息量大的通信; -- 共享内存:无须复制,共享缓冲区直接付附加到进程虚拟地址空间,速度快;但进程间的同步问题操作系统无法实现,必须各进程利用同步工具解决; -- 套接字:作为更通用的接口,传输效率低,主要用于不通机器或跨网络的通信; +- 消息队列:信息会复制两次,会有额外的CPU消耗;不合适频繁或信息量大的通信; +- 共享内存:无须复制,共享缓冲区直接附加到进程虚拟地址空间,速度快;但进程间的同步问题操作系统无法实现,必须各进程利用同步工具解决; +- 套接字:作为更通用的接口,传输效率低,主要用于不同机器或跨网络的通信; - 信号量:常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。 - 信号: 不适用于信息交换,更适用于进程中断控制,比如非法内存访问,杀死某个进程等; @@ -44,7 +44,7 @@ Binder作为Android系统提供的一种IPC机制。首先一个问题就是为 **接下来正面回答这个问题,从5个角度来展开对Binder的分析:** -**(1)从性能的角度** **数据拷贝次数:**Binder数据拷贝只需要一次,而管道、消息队列、Socket都需要2次,但共享内存方式一次内存拷贝都不需要;从性能角度看,Binder性能仅次于共享内存。 +**(1)从性能的角度** :数据拷贝次数,Binder数据拷贝只需要一次,而管道、消息队列、Socket都需要2次,但共享内存方式一次内存拷贝都不需要;从性能角度看,Binder性能仅次于共享内存。 **(2)从稳定性的角度** Binder是基于C/S架构的,简单解释下C/S架构,是指客户端(Client)和服务端(Server)组成的架构,Client端有什么需求,直接发送给Server端去完成,架构清晰明朗,Server端与Client端相对独立,稳定性较好;而共享内存实现方式复杂,没有客户与服务端之别, 需要充分考虑到访问临界资源的并发同步问题,否则可能会出现死锁等问题;从这稳定性角度看,Binder架构优越于共享内存。 @@ -52,18 +52,14 @@ Binder是基于C/S架构的,简单解释下C/S架构,是指客户端(Client) 仅仅从以上两点,各有优劣,还不足以支撑google去采用binder的IPC机制,那么更重要的原因是: **(3)从安全的角度** -传统Linux IPC的接收方无法获得对方进程可靠的UID/PID,从而无法鉴别对方身份;而Android作为一个开放的开源体系,拥有非常多的开发平台,App来源甚广,因此手机的安全显得额外重要;对于普通用户,绝不希望从App商店下载偷窥隐射数据、后台造成手机耗电等等问题,传统Linux IPC无任何保护措施,完全由上层协议来确保。 +传统Linux IPC的接收方无法获得对方进程可靠的UID/PID,从而无法鉴别对方身份;而Android作为一个开放的开源体系,拥有非常多的开发平台,App来源甚广,因此手机的安全显得额外重要;对于普通用户,绝不希望从App商店下载偷窥隐私数据、后台造成手机耗电等等问题,传统Linux IPC无任何保护措施,完全由上层协议来确保。 Android为每个安装好的应用程序分配了自己的UID,故进程的UID是鉴别进程身份的重要标志,前面提到C/S架构,**Android系统中对外只暴露Client端,Client端将任务发送给Server端,Server端会根据权限控制策略,判断UID/PID是否满足访问权限,目前权限控制很多时候是通过弹出权限询问对话框,让用户选择是否运行**。Android 6.0,也称为Android M,在6.0之前的系统是在App第一次安装时,会将整个App所涉及的所有权限一次询问,只要留意看会发现很多App根本用不上通信录和短信,但在这一次性权限权限时会包含进去,让用户拒绝不得,因为拒绝后App无法正常使用,而一旦授权后,应用便可以胡作非为。 -针对这个问题,google在Android M做了调整,不再是安装时一并询问所有权限,而是在App运行过程中,需要哪个权限再弹框询问用户是否给相应的权限,对权限做了更细地控制,让用户有了更多的可控性,但**同时也带来了另一个用户诟病的地方,那也就是权限询问的弹框的次数大幅度增多。**对于Android M平台上,有些App开发者可能会写出让手机异常频繁弹框的App,企图直到用户授权为止,这对用户来说是不能忍的,用户最后吐槽的可不光是App,还有Android系统以及手机厂商,有些用户可能就跳果粉了,这还需要广大Android开发者以及手机厂商共同努力,共同打造安全与体验俱佳的Android手机。 +针对这个问题,google在Android M做了调整,不再是安装时一并询问所有权限,而是在App运行过程中,需要哪个权限再弹框询问用户是否给相应的权限,对权限做了更细地控制,让用户有了更多的可控性,但同时也带来了另一个用户诟病的地方,那也就是权限询问的弹框的次数大幅度增多。对于Android M平台上,有些App开发者可能会写出让手机异常频繁弹框的App,企图直到用户授权为止,这对用户来说是不能忍的,用户最后吐槽的可不光是App,还有Android系统以及手机厂商,有些用户可能就跳果粉了,这还需要广大Android开发者以及手机厂商共同努力,共同打造安全与体验俱佳的Android手机。 Android中权限控制策略有SELinux等多方面手段。传统IPC只能由用户在数据包里填入UID/PID;另外,可靠的身份标记只有由IPC机制本身在内核中添加。其次传统IPC访问接入点是开放的,无法建立私有通道。从安全角度,Binder的安全性更高。 -**说到这,可能有人要反驳**,Android就算用了Binder架构,而现如今Android手机的各种流氓软件,不就是干着这种偷窥隐射,后台偷偷跑流量的事吗?没错,确实存在,但这不能说Binder的安全性不好,因为Android系统仍然是掌握主控权,可以控制这类App的流氓行为,只是对于该采用何种策略来控制,在这方面android的确存在很多有待进步的空间,这也是google以及各大手机厂商一直努力改善的地方之一。在Android 6.0,google对于app的权限问题作为较多的努力,大大收紧的应用权限;另外,在**Google举办的Android Bootcamp 2016**大会中,google也表示在Android 7.0 (也叫Android N)的权限隐私方面会进一步加强加固,比如SELinux,Memory safe language(还在research中)等等,在今年的5月18日至5月20日,google将推出Android N。 - -话题扯远了,继续说Binder。 - **(4)从语言层面的角度** 大家多知道Linux是基于C语言(面向过程的语言),而Android是基于Java语言(面向对象的语句),而对于Binder恰恰也符合面向对象的思想,将进程间通信转化为通过对某个Binder对象的引用调用该对象的方法,而其独特之处在于Binder对象是一个可以跨进程引用的对象,它的实体位于一个进程中,而它的引用却遍布于系统的各个进程之中。可以从一个进程传给其它进程,让大家都能访问同一Server,就像将一个对象或引用赋值给另一个引用一样。Binder模糊了进程边界,淡化了进程间通信过程,整个系统仿佛运行于同一个面向对象的程序之中。从语言层面,Binder更适合基于面向对象语言的Android系统,对于Linux系统可能会有点“水土不服”。 @@ -73,11 +69,11 @@ Android中权限控制策略有SELinux等多方面手段。传统IPC只能由用 总所周知,Linux内核是开源的系统,所开放源代码许可协议GPL保护,该协议具有“病毒式感染”的能力,怎么理解这句话呢?受GPL保护的Linux Kernel是运行在内核空间,对于上层的任何类库、服务、应用等运行在用户空间,一旦进行SysCall(系统调用),调用到底层Kernel,那么也必须遵循GPL协议。 -而Android 之父 Andy Rubin对于GPL显然是不能接受的,为此,Google巧妙地将GPL协议控制在内核空间,将用户空间的协议采用Apache-2.0协议(允许基于Android的开发商不向社区反馈源码),同时在GPL协议与Apache-2.0之间的Lib库中采用BSD证授权方法,有效隔断了GPL的传染性,仍有较大争议,但至少目前缓解Android,让GPL止步于内核空间,这是Google在GPL Linux下 开源与商业化共存的一个成功典范。 +而Android 之父 Andy Rubin对于GPL显然是不能接受的,为此,Google巧妙地将GPL协议控制在内核空间,将用户空间的协议采用Apache-2.0协议(允许基于Android的开发商不向社区反馈源码),同时在GPL协议与Apache-2.0之间的Lib库中采用BSD证授权方法,有效隔断了GPL的传染性,仍有较大争议,但至少目前缓解Android,让GPL止步于内核空间,这是Google在GPL Linux下开源与商业化共存的一个成功典范。 **有了这些铺垫,我们再说说Binder的今世前缘** -Binder是基于开源的 [OpenBinder](https://link.zhihu.com/?target=http%3A//www.angryredplanet.com/~hackbod/openbinder/docs/html/BinderIPCMechanism.html)实现的,OpenBinder是一个开源的系统IPC机制,最初是由 [Be Inc.](https://link.zhihu.com/?target=https%3A//en.wikipedia.org/wiki/Be_Inc.) 开发,接着由[Palm, Inc.](https://link.zhihu.com/?target=https%3A//en.wikipedia.org/wiki/Palm%2C_Inc.)公司负责开发,现在OpenBinder的作者在Google工作,既然作者在Google公司,在用户空间采用Binder 作为核心的IPC机制,再用Apache-2.0协议保护,自然而然是没什么问题,减少法律风险,以及对开发成本也大有裨益的,那么从公司战略角度,Binder也是不错的选择。 +Binder是基于开源的[OpenBinder](https://link.zhihu.com/?target=http%3A//www.angryredplanet.com/~hackbod/openbinder/docs/html/BinderIPCMechanism.html)实现的,OpenBinder是一个开源的系统IPC机制,最初是由 [Be Inc.](https://link.zhihu.com/?target=https%3A//en.wikipedia.org/wiki/Be_Inc.) 开发,接着由[Palm, Inc.](https://link.zhihu.com/?target=https%3A//en.wikipedia.org/wiki/Palm%2C_Inc.)公司负责开发,现在OpenBinder的作者在Google工作,既然作者在Google公司,在用户空间采用Binder 作为核心的IPC机制,再用Apache-2.0协议保护,自然而然是没什么问题,减少法律风险,以及对开发成本也大有裨益的,那么从公司战略角度,Binder也是不错的选择。 另外,再说一点关于OpenBinder,在2015年OpenBinder以及合入到Linux Kernel主线 3.19版本,这也算是Google对Linux的一点回馈吧。 @@ -87,10 +83,10 @@ Binder是基于开源的 [OpenBinder](https://link.zhihu.com/?target=http%3A//ww **最后,简单讲讲Android Binder架构** -Binder在Android系统中江湖地位非常之高。在Zygote孵化出system_server进程后,在system_server进程中出初始化支持整个Android framework的各种各样的Service,而这些Service从大的方向来划分,分为Java层Framework和Native Framework层(C++)的Service,几乎都是基于BInder IPC机制。 +Binder在Android系统中江湖地位非常之高。在Zygote孵化出system_server进程后,在system_server进程中出初始化支持整个Android framework的各种各样的Service,而这些Service从大的方向来划分,分为Java层Framework和Native Framework层(C++)的Service,几乎都是基于BInder IPC机制: -1. **Java framework:作为Server端继承(或间接继承)于Binder类,Client端继承(或间接继承)于BinderProxy类。**例如 ActivityManagerService(用于控制Activity、Service、进程等) 这个服务作为Server端,间接继承Binder类,而相应的ActivityManager作为Client端,间接继承于BinderProxy类。 当然还有PackageManagerService、WindowManagerService等等很多系统服务都是采用C/S架构; -2. **Native Framework层:这是C++层,作为Server端继承(或间接继承)于BBinder类,Client端继承(或间接继承)于BpBinder。**例如MediaPlayService(用于多媒体相关)作为Server端,继承于BBinder类,而相应的MediaPlay作为Client端,间接继承于BpBinder类。 +1. Java framework:作为Server端继承(或间接继承)于Binder类,Client端继承(或间接继承)于BinderProxy类。例如 ActivityManagerService(用于控制Activity、Service、进程等) 这个服务作为Server端,间接继承Binder类,而相应的ActivityManager作为Client端,间接继承于BinderProxy类。 当然还有PackageManagerService、WindowManagerService等等很多系统服务都是采用C/S架构; +2. Native Framework层:这是C++层,作为Server端继承(或间接继承)于BBinder类,Client端继承(或间接继承)于BpBinder。例如MediaPlayService(用于多媒体相关)作为Server端,继承于BBinder类,而相应的MediaPlay作为Client端,间接继承于BpBinder类。 @@ -100,21 +96,21 @@ Binder在Android系统中江湖地位非常之高。在Zygote孵化出system_ser 前面人都说了Binder的优点,我来讲故事 -1. 当年Andy Rubin有个公司 Palm 做掌上设备的 就是当年那种PDA 有个系统叫PalmOS 后来palm被收购了以后 Andy Rubin 创立了Android +1. 当年Andy Rubin有个公司Palm做掌上设备的就是当年那种PDA有个系统叫PalmOS后来palm被收购了以后 Andy Rubin创立了Android -2. Palm收购过一个公司叫 Be 里面有个移动系统 叫 BeOS 进程通信自己学了个实现 叫Binder 由一个叫 Dianne Hackbod的人开发并维护 后来Binder 也被用到了 PalmOS里 +2. Palm收购过一个公司叫Be里面有个移动系统叫BeOS,进程通信自己写了个实现叫Binder由一个叫Dianne Hackbod的人开发并维护后来Binder也被用到了PalmOS里 -3. Android创立了以后 Andy从Palm带走了一大批人,其中就有Dianne。Dianne成为安卓系统总架构师。 +3. Android创立了以后Andy从Palm带走了一大批人,其中就有Dianne。Dianne成为安卓系统总架构师。 -- 如果你是她,你会选择用a.Linux已有的进程通信手段吗? 不会,要不当年也不会搞个新东西出来 +- 如果你是她,你会选择用Linux已有的进程通信手段吗? 不会,要不当年也不会搞个新东西出来 -- 重写一个新东西 也不会 binder反正是自己写的开源库 +- 重写一个新东西?也不会,binder反正是自己写的开源库 -- 用binder 已经被两个公司用过 而且是自己写的 可靠放心 +- 用binder?已经被两个公司用过而且是自己写的可靠放心 -我是她我就选C +我是她我就用binder。 -你可以看到 如果当年Dianne没有加入Be 或者Be没有被收购 ,又或者Dianne没有和Andy加入Android 那Android也不一定会用binder。 +你可以看到 如果当年Dianne没有加入Be或者Be没有被收购 ,又或者Dianne没有和Andy加入Android 那Android也不一定会用binder。 @@ -146,7 +142,7 @@ Binder是一种架构,这种架构提供了服务端接口、Binder驱动、 -每个Android的进程,只能运行在自己进程所拥有的虚拟地址空间。对应一个4GB的虚拟地址空间,其中3GB是用户空间,1GB是内核空间,当然内核空间的大小是可以通过参数配置调整的。对于用户空间,不同进程之间彼此是不能共享的,而内核空间却是可共享的。Client进程向Server进程通信,恰恰是利用进程间可共享的内核内存空间来完成底层通信工作的,Client端与Server端进程往往采用ioctl等方法跟内核空间的驱动进行交互。 +每个Android的进程,只能运行在自己进程所拥有的虚拟地址空间。对应一个4GB的虚拟地址空间,其中3GB是用户空间,1GB是内核空间,当然内核空间的大小是可以通过参数配置调整的。对于用户空间,不同进程之间彼此是不能共享的,而内核空间却是可共享的。Client进程向Server进程通信,恰恰是利用进程间可共享的内核内存空间来完成底层通信工作的,Client端与Server端进程往往采用ioctl(设备驱动程序中设备控制接口函数,进程与内核通信的一种方法)等方法跟内核空间的驱动进行交互。 @@ -154,7 +150,7 @@ Binder通信采用C/S架构,从组件视角来说,包含Client、Server、Se ![ServiceManager](http://gityuan.com/images/binder/prepare/IPC-Binder.jpg) -可以看出无论是注册服务和获取服务的过程都需要ServiceManager,需要注意的是此处的Service Manager是指Native层的ServiceManager(C++),并非指framework层的ServiceManager(Java)。ServiceManager是整个Binder通信机制的大管家,是Android进程间通信机制Binder的守护进程,要掌握Binder机制,首先需要了解系统是如何首次[启动Service Manager](http://gityuan.com/2015/11/07/binder-start-sm/)。当Service Manager启动之后,Client端和Server端通信时都需要先[获取Service Manager](http://gityuan.com/2015/11/08/binder-get-sm/)接口,才能开始通信服务。 +可以看出无论是注册服务和获取服务的过程都需要ServiceManager,需要注意的是此处的Service Manager是指Native层的ServiceManager(C++),并非指framework层的ServiceManager(Java)。ServiceManager是整个Binder通信机制的大管家,是Android进程间通信机制Binder的守护进程。 图中Client/Server/ServiceManage之间的相互通信都是基于Binder机制。既然基于Binder机制通信,那么同样也是C/S架构,则图中的3大步骤都有相应的Client端与Server端。 @@ -189,8 +185,14 @@ BpBinder(客户端)和BBinder(服务端)都是Android中Binder通信相关的代 +--- + +- [上一篇:8.虚拟机](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/8.%E8%99%9A%E6%8B%9F%E6%9C%BA.md) -- [上一篇:7.嵌入式系统](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/7.%E5%B5%8C%E5%85%A5%E5%BC%8F%E7%B3%BB%E7%BB%9F.md) + +- [下一篇:2.Android线程间通信之Handler消息机制](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/2.Android%E7%BA%BF%E7%A8%8B%E9%97%B4%E9%80%9A%E4%BF%A1%E4%B9%8BHandler%E6%B6%88%E6%81%AF%E6%9C%BA%E5%88%B6.md) + + --- From cd425547bca4d9585c4c3f435bdc32fc6eef5bdf Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 4 Dec 2020 19:48:54 +0800 Subject: [PATCH 040/213] format text --- ...13\351\227\264\351\200\232\344\277\241.md" | 2 - ...10\346\201\257\346\234\272\345\210\266.md" | 85 +- ...roid Framework\346\241\206\346\236\266.md" | 7 +- ...ManagerService\347\256\200\344\273\213.md" | 15 +- ...10\346\201\257\350\216\267\345\217\226.md" | 11 +- ...30\345\210\266\345\237\272\347\241\200.md" | 5 +- ...30\345\210\266\345\216\237\347\220\206.md" | 1613 +++++++++++++++++ ...ManagerService\347\256\200\344\273\213.md" | 9 +- ...ManagerService\347\256\200\344\273\213.md" | 10 +- 9 files changed, 1704 insertions(+), 53 deletions(-) diff --git "a/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" "b/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" index 68b17d24..7263e837 100644 --- "a/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" +++ "b/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" @@ -183,8 +183,6 @@ BpBinder(客户端)和BBinder(服务端)都是Android中Binder通信相关的代 - - --- - [上一篇:8.虚拟机](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/8.%E8%99%9A%E6%8B%9F%E6%9C%BA.md) diff --git "a/OperatingSystem/AndroidKernal/2.Android\347\272\277\347\250\213\351\227\264\351\200\232\344\277\241\344\271\213Handler\346\266\210\346\201\257\346\234\272\345\210\266.md" "b/OperatingSystem/AndroidKernal/2.Android\347\272\277\347\250\213\351\227\264\351\200\232\344\277\241\344\271\213Handler\346\266\210\346\201\257\346\234\272\345\210\266.md" index df78f0cf..dbcaf9bb 100644 --- "a/OperatingSystem/AndroidKernal/2.Android\347\272\277\347\250\213\351\227\264\351\200\232\344\277\241\344\271\213Handler\346\266\210\346\201\257\346\234\272\345\210\266.md" +++ "b/OperatingSystem/AndroidKernal/2.Android\347\272\277\347\250\213\351\227\264\351\200\232\344\277\241\344\271\213Handler\346\266\210\346\201\257\346\234\272\345\210\266.md" @@ -15,7 +15,7 @@ Binder/Socket用于进程间通信,而Handler消息机制用于同进程的线 - Message:消息分为硬件产生的消息(如按钮、触摸)和软件生成的消息;Message中有一个用于处理消息的Handler; - MessageQueue:消息队列的主要功能向消息池投递消息(MessageQueue.enqueueMessage)和取走消息池的消息(MessageQueue.next);MessageQueue有一组待处理的Message; -- Handler:消息辅助类,主要功能向消息池发送各种消息事件(Handler.sendMessage)和处理相应消息事件(Handler.handleMessage);Handler中有Looper和MessageQueue。 +- Handler:消息辅助类,主要功能向消息池发送各种消息事件(Handler.sendMessage)和处理相应消息事件(Handler.handleMessage);Handler中有Looper和MessageQueue的成员变量。 - Looper:不断循环执行(Looper.loop),按分发机制将消息分发给目标处理者。Looper有一个MessageQueue消息队列; @@ -57,7 +57,7 @@ public Handler(@NonNull Looper looper) { } public Handler(@Nullable Callback callback, boolean async) { - // 匿名类、内部类和本地类都必须申请为static,否则会警告可能出现内存泄露 + // 匿名类、内部类和本地类都必须申请为static,否则会警告可能出现内存泄露 if (FIND_POTENTIAL_LEAKS) { final Class klass = getClass(); if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) && @@ -73,7 +73,7 @@ public Handler(@Nullable Callback callback, boolean async) { "Can't create handler inside thread " + Thread.currentThread() + " that has not called Looper.prepare()"); } - // 获取Looper中的MessageQueue + // 获取Looper中的MessageQueue赋值给当前的MessageQueue变量 mQueue = mLooper.mQueue; // 回调接口 mCallback = callback; @@ -181,11 +181,11 @@ private Looper(boolean quitAllowed) { ```java /** - * Initialize the current thread as a looper, marking it as an - * application's main looper. The main looper for your application - * is created by the Android environment, so you should never need - * to call this function yourself. See also: {@link #prepare()} - */ +* Initialize the current thread as a looper, marking it as an +* application's main looper. The main looper for your application +* is created by the Android environment, so you should never need +* to call this function yourself. See also: {@link #prepare()} +*/ public static void prepareMainLooper() { prepare(false); synchronized (Looper.class) { @@ -393,11 +393,11 @@ public static void loop() { Trace.traceEnd(traceTag); } } - // 恢复调用者信息 + // 恢复调用者信息 // Make sure that during the course of dispatching the // identity of the thread wasn't corrupted. final long newIdent = Binder.clearCallingIdentity(); - // 将Message放入消息池 + // 将Message放入消息池 msg.recycleUnchecked(); } } @@ -503,8 +503,8 @@ public void handleMessage(@NonNull Message msg) { } /** - * Handle system messages here. - */ + * Handle system messages here. + */ public void dispatchMessage(@NonNull Message msg) { if (msg.callback != null) { // 如果Message存在回调方法,就回调Message.callback.run()方法 @@ -571,7 +571,7 @@ public final class MessageQueue { if (nextPollTimeoutMillis != 0) { Binder.flushPendingCommands(); } - // 该方法的作用是从消息队列中取出一个消息。MessageQueue中没有保存消息队列,真正的消息队列在JNI的C代码中 + // 该方法的作用是从消息队列中取出一个消息。MessageQueue中没有保存消息队列,真正的消息队列在JNI的C代码中 // 也就是在C环境中创建了一个NativeMessageQueue数据对象。该方法的第一个参数是int型变量,在C环境中该变量 // 会被强制转换为一个NativeMessageQueue对象。如果消息队列中没消息,当前线程会被挂起。 // 堵塞操作,当等待nextPollTimeoutMillis时长或消息队列别唤醒,都会返回 @@ -640,7 +640,7 @@ public final class MessageQueue { } mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers); } - // 空闲回调函数 + // 空闲回调函数 // Run the idle handlers. // We only ever reach this code block during the first iteration. for (int i = 0; i < pendingIdleHandlerCount; i++) { @@ -770,7 +770,7 @@ public final class MessageQueue { synchronized (this) { Message p = mMessages; - // 从消息队列的头开始,移除所有符合条件的消息 + // 从消息队列的头开始,移除所有符合条件的消息 // Remove all messages at front. while (p != null && p.target == h && (object == null || p.obj == object)) { @@ -779,7 +779,7 @@ public final class MessageQueue { p.recycleUnchecked(); p = n; } - // 移除剩余符合条件的消息 + // 移除剩余符合条件的消息 // Remove all messages after front. while (p != null) { Message n = p.next; @@ -807,7 +807,7 @@ public final class MessageQueue { -有关内存泄露请猛戳[内存泄露][1] +有关内存泄露请猛戳[内存泄露](https://github.com/CharonChui/AndroidNote/blob/master/BasicKnowledge/%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F.md) 在上面分析Handler类源码时,其构造函数中第一部分的代码就是 匿名类、内部类和本地类都必须申请为static,否则会警告可能出现内存泄露。那为什么会导致内存泄露呢? @@ -815,7 +815,7 @@ public final class MessageQueue { Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { - // do something. + // do something. } } ``` @@ -832,24 +832,25 @@ Handler mHandler = new Handler() { ```java public class SampleActivity extends Activity { - private final Handler mHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - // do something - } - } + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + // do something + } + } - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - // 发送一个10分钟后执行的一个消息 - mHandler.postDelayed(new Runnable() { - @Override - public void run() { } - }, 600000); - - // 结束当前的Activity - finish(); + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // 发送一个10分钟后执行的一个消息 + mHandler.postDelayed(new Runnable() { + @Override + public void run() { } + }, 600000); + + // 结束当前的Activity + finish(); + } } ``` 在`finish()`的时候,该`Message`还没有被处理,`Message`持有`Handler`,`Handler`持有`Activity`,这样会导致该`Activity`不会被回收,就发生了内存泄露. @@ -861,14 +862,13 @@ public class SampleActivity extends Activity { - 如果`Handler`是被`delay`的`Message`持有了引用,那么在`Activity`的`onDestroy()`方法要调用`Handler`的`remove*`方法,把消息对象从消息队列移除就行了。 - 关于`Handler.remove*`方法 - `removeCallbacks(Runnable r)` ——清除r匹配上的Message。 - - `removeC4allbacks(Runnable r, Object token)` ——清除r匹配且匹配token(Message.obj)的Message,token为空时,只匹配r。 - - `removeCallbacksAndMessages(Object token)` ——清除token匹配上的Message。 + - `removeCallbacks(Runnable r, Object token)` ——清除r匹配且匹配token(Message.obj)的Message,token为空时,只匹配r。 + - `removeCallbacksAndMessages(Object token)` ——清除所有callback以及token匹配上的Message,如果token是null就会清楚所有callback和message。我们更多需要的是清除以该`Handler`为`target`的所有`Message(Callback)`就调用如下方法即可`handler.removeCallbacksAndMessages(null)`; - `removeMessages(int what)` ——按what来匹配 - `removeMessages(int what, Object object)` ——按what来匹配 - 我们更多需要的是清除以该`Handler`为`target`的所有`Message(Callback)`就调用如下方法即可`handler.removeCallbacksAndMessages(null)`; + - 将`Handler`声明为静态类。 静态类不持有外部类的对象,所以你的`Activity`可以随意被回收。但是不持有`Activity`的引用,如何去操作`Activity`中的一些对象? 这里要用到弱引用 - ```java public class MyActivity extends Activity { private MyHandler mHandler; @@ -905,9 +905,14 @@ public class MyActivity extends Activity { } ``` -[1]:(https://github.com/CharonChui/AndroidNote/blob/master/BasicKnowledge/%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F.md) +--- + + + +- [上一篇:1.Android进程间通信](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/1.Android%E8%BF%9B%E7%A8%8B%E9%97%B4%E9%80%9A%E4%BF%A1.md) +- [下一篇:3.Android Framework框架](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/3.Android%20Framework%E6%A1%86%E6%9E%B6.md) diff --git "a/OperatingSystem/AndroidKernal/3.Android Framework\346\241\206\346\236\266.md" "b/OperatingSystem/AndroidKernal/3.Android Framework\346\241\206\346\236\266.md" index 100b61d7..f550d1ee 100644 --- "a/OperatingSystem/AndroidKernal/3.Android Framework\346\241\206\346\236\266.md" +++ "b/OperatingSystem/AndroidKernal/3.Android Framework\346\241\206\346\236\266.md" @@ -33,7 +33,7 @@ AmS的作用是管理所有应用程序中的Activity。 - Activity类:该类为APK程序中的一个最小运行单元,一个APK程序中可以包含多个Activity对象,ActivityThread类会根据用户操作选择运行哪个Activity对象。 - PhoneWindow类:该类继承于Window类,同时,PhoneWindow类内部包含了一个DecorView对象。简而言之,PhoneWindow是把一个FrameLayout进行了一定的包装,并提供了一组通用的窗口操作接口。 - Window类:该类提供了一组通用的窗口(Window)操作API,这里的窗口仅仅是程序层面上的,WmS所管理的窗口并不是Window类,而是一个View或者ViewGroup类,一般就是指DecorView类,即一个DecorView就是WmS所管理的一个窗口。Window是一个abstract类型。 -- DecorView类:该类是一个FrameLayout的子类,并且是PhoneWindow中的一个内部类。Decor的英文是Decoration,即”装饰“的意思,DecorView就是对普通的FrameLayout进行了一定的装饰,比如添加一个通用的Titlebar,并相应特定的按键消息等。 +- DecorView类:该类是一个FrameLayout的子类,并且是PhoneWindow中的一个内部类。Decor的英文是Decoration,即”装饰“的意思,DecorView就是对普通的FrameLayout进行了一定的装饰,比如添加一个通用的Titlebar,并响应特定的按键消息等。 - ViewRoot类:WmS管理客户端窗口时,需要通知客户端进行某种操作,这些都是通过异步消息完成的,实现的方式就是使用Handler,ViewRoot类就是继承于Handler,其作用主要是接收WmS的通知。 - W类:该类继承于Binder,并且是ViewRoot的一个内部类。 - WindowManager类:客户端要申请创建一个窗口,而具体创建窗口的任务是由WmS完成的,WindowManager类就像是一个部门经理,谁有什么需求就告诉它,由它和WmS进行交互,客户端不能直接和WmS进行交互。 @@ -69,7 +69,7 @@ Linux驱动和Framework相关的主要包含两部分: - 窗口(Window):这是一个纯语义的说法,即程序员所看到的屏幕上的某个独立的界面,比如一个带有TitleBar的Activity界面、一个对话框、一个Menu菜单等,这些都称之为窗口。从WmS的角度来讲,窗口是接收用户消息的最小单元,WmS内部用特定的类表示一个窗口,以实现对窗口的管理。WmS接收到用户消息后,首先要判断这个消息属于哪个窗口,然后通过IPC调用把这个消息传递给客户端的ViewRoot类。 - Window类:该类在android.view包中,是一个abstract类,该类是对包含有可视界面的窗口的一种包装,所谓的可视界面就是指各种View或者ViewGroup,一般可以通过res/layout目录下的xml文件描述。 -- ViewRoot类:该类是android.view包中,客户端申请创建窗口时需要一个客户端代理,用以和WmS进行交互,这个就是ViewRoot的功能,每个客户端的窗口都会对应一个ViewRoot类。 +- ViewRoot类:该类是android.view包中,客户端申请创建窗口时需要一个客户端代理,用以和WmS进行交互,这个就是ViewRoot的功能,每个客户端的窗口都会对应一个ViewRoot类。ViewRoot类在Android2.2之后就被ViewRootImpl替换了。但是为了方便,后面还是会用ViewRoot类来介绍。 - W类:该类是ViewRoot类的一个内部类,继承于Binder,用于想WmS提供一个IPC接口,从而让WmS控制窗口客户端的行为。 描述一个窗口之所以有这么多类的原因在于,窗口的概念存在于客户端和服务端(WmS)之中,客户端所理解的窗口和服务端理解的窗口是不同的,因此,在客户端和服务端会用不同的类来描述窗口。比如在客户端,用户能看到的窗口一般是View或者ViewGroup组成的窗口,而与Activity对应的窗口却是一个DecorView类,而具备常规Phone操作的接口却又是一个PhoneWindow类。所以无论是在客户端还是服务端,对窗口都有不同层面的抽象。 @@ -94,7 +94,10 @@ Context在应用程序开发中会经常被使用,在一般的计算机书记 +--- +- [上一篇:2.Android线程间通信之Handler消息机制](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/2.Android%E7%BA%BF%E7%A8%8B%E9%97%B4%E9%80%9A%E4%BF%A1%E4%B9%8BHandler%E6%B6%88%E6%81%AF%E6%9C%BA%E5%88%B6.md) +- [下一篇:4.ActivityManagerService简介](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/4.ActivityManagerService%E7%AE%80%E4%BB%8B.md) diff --git "a/OperatingSystem/AndroidKernal/4.ActivityManagerService\347\256\200\344\273\213.md" "b/OperatingSystem/AndroidKernal/4.ActivityManagerService\347\256\200\344\273\213.md" index 0a5b9007..3e88e552 100644 --- "a/OperatingSystem/AndroidKernal/4.ActivityManagerService\347\256\200\344\273\213.md" +++ "b/OperatingSystem/AndroidKernal/4.ActivityManagerService\347\256\200\344\273\213.md" @@ -1,6 +1,6 @@ # 4.ActivityManagerService简介 -ActivityManagerService.java文件,简称AmS,是Android内核的三大核心功能之一,另外两个是WindowManagerService.java和View.java。Activity的管理实际上由三个主要类完成,分别是AmS、ActivityRecord及ActivityTask。AmS内部用ActivityRecord来表示一个Activity对象。ActivityStack是一个描述Task的类,所有和Task相关的数据以及控制都由ActivityStack来实现。 +ActivityManagerService简称AmS,是Android内核的三大核心功能之一,另外两个是WindowManagerService和View。Activity的管理实际上由三个主要类完成,分别是AmS、ActivityRecord及ActivityTask。AmS内部用ActivityRecord来表示一个Activity对象。ActivityStack是一个描述Task的类,所有和Task相关的数据以及控制都由ActivityStack来实现。 AmS所提供的主要功能包括以下几项: @@ -16,7 +16,7 @@ AmS所提供的主要功能包括以下几项: 在Android中,Activity调度的基本思路是:各应用进程要启动新的Activity或者停止当前的Activity,都要首先报告给AmS,而不能“擅自处理”。AmS在内部为所有应用程序都做了记录,当AmS接到启动或停止的报告时,首先更新内部记录,然后再通知相应客户进程运行或者停止指定的Activity。由于AmS内部有所有Activity的记录,也就理所当然地能够调度这些Activity,并根据Activity和系统内存的状态自动杀死后台的Activity。 -具体的讲,启动一个Activity有以下集中方式: +具体的讲,启动一个Activity有以下几种方式: - 在应用程序中调用startActivity()启动指定的Activity。 - 在Home程序中单击一个应用图标,启动新的Activity。 @@ -40,7 +40,7 @@ ProcessRecord记录一个进程中的相关信息,该类中内部变量可以 AmS中使用HistoryRecord数据类来保存每个Activity的信息,这里非常奇怪,Activity本身也是一个类,为什么还要用HistoryRecord来保存Activity的信息,而不是直接用Activity呢?这是因为Activity是具体的功能类,这就好比每一个读者都是一个Activity,而学校要为每一个读者建立一个档案,这些档案中并不包含每个读者具体的学习能力,而只是学生的籍贯信息、姓名、出生日期等,HistoryRecord就是AmS为每一个Activity建立的档案,该数据类中的变量主要包含两部分: - 环境信息:该Activity的工作环境,比如隶属于哪个Package,所在的进程名称、文件路径、数据路径、图标主题等。 -- 运行状态信息:比如idle、stop、finishing等,这些变量一般问boolean类型,这些状态值与应用程序中的onCreate、onPause、onStart等状态有所不同。 +- 运行状态信息:比如idle、stop、finishing等,这些变量一般为boolean类型,这些状态值与应用程序中的onCreate、onPause、onStart等状态有所不同。 HistoryRecord类也是一个Binder,它基于IApplication.Stub类,因此它可以被IPC调用,一般是在WmS中进行该对象的IPC调用。 @@ -131,7 +131,7 @@ TaskRecord中并没有该任务中所包含的Activity的列表,比如ArrayLis -## startActivi()启动流程 +## startActiviy()启动流程 @@ -142,6 +142,13 @@ TaskRecord中并没有该任务中所包含的Activity的列表,比如ArrayLis +--- + +- [上一篇:3.Android Framework框架](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/3.Android%20Framework%E6%A1%86%E6%9E%B6.md) +- [下一篇:5.Android消息获取](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/5.Android%E6%B6%88%E6%81%AF%E8%8E%B7%E5%8F%96.md) + + + --- - 邮箱 :charon.chui@gmail.com diff --git "a/OperatingSystem/AndroidKernal/5.Android\346\266\210\346\201\257\350\216\267\345\217\226.md" "b/OperatingSystem/AndroidKernal/5.Android\346\266\210\346\201\257\350\216\267\345\217\226.md" index f731b4e9..3fe0e833 100644 --- "a/OperatingSystem/AndroidKernal/5.Android\346\266\210\346\201\257\350\216\267\345\217\226.md" +++ "b/OperatingSystem/AndroidKernal/5.Android\346\266\210\346\201\257\350\216\267\345\217\226.md" @@ -24,7 +24,7 @@ 1. 首先,InputReader线程会持续调用输入设备的驱动,读取所有用户输入的消息,该线程和InputDispatcher线程都在系统进程(system_process)空间中运行。InputDispatcher线程从自己的消息队列中取出原始消息,取出的消息有可能经过两种方式进行派发。 - 经过管道(Pipe)直接派发到客户窗口中。 - 先派发到WmS中,由WmS经过一定的处理,如果WmS没有处理该消息,则再派发给客户窗口中,否则,不派发到客户窗口。 -2. 应用程序添加窗口时,会在本地创建一个ViewRoot对象,然后通过IPC调用WmS中的Session对象的addWindow()方法,从而请求WmS创建一个窗口。WmS会把窗口的相关信息保存在内部的一个窗口列表类InputMonitore中,然后使用InputManager类把这些窗口信息传递给InputDispatcher线程。传递的过程中,InputManager类需要调用JNI代码,把这些窗口信息传递给NativeInputManager对象中。 +2. 应用程序添加窗口时,会在本地创建一个ViewRoot对象,然后通过IPC调用WmS中的Session对象的addWindow()方法,从而请求WmS创建一个窗口。WmS会把窗口的相关信息保存在内部的一个窗口列表类InputMonitor中,然后使用InputManager类把这些窗口信息传递给InputDispatcher线程。传递的过程中,InputManager类需要调用JNI代码,把这些窗口信息传递给NativeInputManager对象中。 3. 当InputDispatcher得到用户消息后,会根据NativeInputManager中保存的所有窗口信息判断当前的活动窗口是哪个,并把消息传递给该活动窗口。另外,如果是按键消息,InputDispatcher会先回调InputManager中定义的回调函数,这既会回调InputMonitor中的回调函数,又会回调WmS中定义的相关函数,所以这些回调函数的返回值类型是boolean。对于系统按键消息,比如“Home键”、电话按键等,WmS内部会按默认的方式处理,并返回false,从而InputDispatcher不会继续把这些按键消息传递给客户窗口。对于触摸屏消息,InputDispatcher则直接传递给客户窗口。 4. 在InputDispatcher和客户窗口之间使用了管道(Pipe)机制进行消息传递。Pipe是Linux的一种系统调用,Linux会在内核地址空间中开辟一段共享内存,并产生一个Pipe对象。每个Pipe对象内部都会自动创建两个文件描述符,一个用于读,另一个用于写。应用程序可以调用pipe()函数产生一个Pipe对象,并获得该对象中的读、写文件描述符。文件描述符是全局唯一的,从而使得两个进程之间可以借助这两个描述符,一个往管道中写数据,另一个从管道中读数据。管道只能是单向的,因此,如果两个进程要进行双向消息传递,必须创建两个管道。当客户窗口请求WmS创建窗口时,WmS内部会创建两个管道,其中一个管道用于InputDispatcher向客户窗口传递消息,另一个用户客户窗口向InputDispatcher报告消息的执行结果。因此,有多少个客户窗口,就有多少个管道与InputDispatcher相连。 5. 由于创建管道属于Linux系统调用,Java不能直接调用,另外也由于程序结构的需要,Android中把调用管道的相关操作封装到了InputChannel.java类中,同时该类中保存了InputDispatcher和客户窗口的管道收、发描述符。因此,InputChannel也可以理解为一个“通道”,在InputDispatcher端存在一个服务端消息通道(serverChannel)。在客户窗口端存在一个客户端消息通道(clientChannel)。管道和通道的区别是,管道是Linux系统的概念,使用pipe()系统调用就可以创建一个管道,而通道是Android内部定义的一个概念,主要保存了通信双发所使用的管道描述符。 @@ -54,6 +54,15 @@ + +--- + +- [上一篇:4.ActivityManagerService简介](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/4.ActivityManagerService%E7%AE%80%E4%BB%8B.md) +- [下一篇:6.屏幕绘制基础](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/6.%E5%B1%8F%E5%B9%95%E7%BB%98%E5%88%B6%E5%9F%BA%E7%A1%80.md) + + + + --- - 邮箱 :charon.chui@gmail.com diff --git "a/OperatingSystem/AndroidKernal/6.\345\261\217\345\271\225\347\273\230\345\210\266\345\237\272\347\241\200.md" "b/OperatingSystem/AndroidKernal/6.\345\261\217\345\271\225\347\273\230\345\210\266\345\237\272\347\241\200.md" index b4f24bb7..95e4f084 100644 --- "a/OperatingSystem/AndroidKernal/6.\345\261\217\345\271\225\347\273\230\345\210\266\345\237\272\347\241\200.md" +++ "b/OperatingSystem/AndroidKernal/6.\345\261\217\345\271\225\347\273\230\345\210\266\345\237\272\347\241\200.md" @@ -7,7 +7,7 @@ Android的屏幕绘制架构如下图: ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_draw_process_archi.jpg) - SurfaceFlinger服务进程:简称sf。该进程在整个系统中只有一个实例,在系统开机后自动运行,它的作用是给每个客户端分配窗口,程序中用Surface类表示这个窗口。正如Surface字面的意义,它是一个平面,即每个窗口是一个平面,每个平面在程序中都对应一段内存,也就是所谓的屏幕缓冲区,不同窗口的缓冲区大小不同,这取决于该窗口的大小,即宽度和高度,一般来讲缓冲区的大小为宽度x高度。sf的客户端必须使用SurfaceFlinger的客户端接口驱动来和sf打交道,系统中使用该接口驱动的最重要的进程就是SystemServer进程。 -- 当一个APK程序需要创建窗口时,会使用WindowManager类的addView()函数,该函数会创建一个ViewRoot对象,而ViewRoot类中会使用Surface的无参构造函数创建一个Surface对象,此时该Surface仅仅是一个空壳,然后调用WindowManager类向WmS服务发起一个请求,请求的参数中包含该Surface对象。虽然它表示的是一个窗口,但它必须经过初始化后才真正能够对应一个再屏幕上显示的窗口,而初始化的本质就是给该Surface对象分配一段屏幕缓冲区的内存。 +- 当一个APK程序需要创建窗口时,会使用WindowManager类的addView()函数,该函数会创建一个ViewRoot(ViewRoot类在Android2.2之后就被ViewRootImpl替换了,这里因为方便后面还是会叫做ViewRoot)对象,而ViewRoot类中会使用Surface的无参构造函数创建一个Surface对象,此时该Surface仅仅是一个空壳,然后调用WindowManager类向WmS服务发起一个请求,请求的参数中包含该Surface对象。虽然它表示的是一个窗口,但它必须经过初始化后才真正能够对应一个再屏幕上显示的窗口,而初始化的本质就是给该Surface对象分配一段屏幕缓冲区的内存。 - WmS收到这个请求后,会通过Surface类的JNI调用到SurfaceFlinger_client驱动,通过该client接口驱动请求sf进程创建指定的窗口。于是sf创建一段屏幕缓冲区,并在sf内部记录该窗口,然后sf会把该窗口的缓冲区地址传递给WmS,WmS再用这个地址去初始化APK程序传入的Surface对象,并最终回到APK程序中。此时APK程序中的Surface对象是一个真正的Surface对象了,因为它包含的屏幕缓冲区已经由sf创建并备案了。 - APK程序有了这个Surface后,就可以给这个平面上绘制任意的内容了,比如绘制矩阵、绘制文本、绘制图片等。然后Surface类本质上仅仅表示了一个平面,而绘制不同图片显然是一种操作,而不是一段数据,因此Android中使用了一个叫做Skia的绘图驱动库,该库使用C/C++语言编写而成,其作用就是能够进行各种平面绘制。在程序中用Canvas类来表示这个功能对象,Canvas类有很多绘制函数,比如drawColor()、drawLine()等。Surface类包含了一个函数lockCanvas(),APK应用程序可以通过该函数返回一个Canvas功能对象,然后就可以调用该对象的各种绘制函数完成对平面的绘制。 @@ -40,7 +40,10 @@ Android的屏幕绘制架构如下图: +--- +- [上一篇:5.Android消息获取](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/5.Android%E6%B6%88%E6%81%AF%E8%8E%B7%E5%8F%96.md) +- [下一篇:7.View绘制原理](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/7.View%E7%BB%98%E5%88%B6%E5%8E%9F%E7%90%86.md) diff --git "a/OperatingSystem/AndroidKernal/7.View\347\273\230\345\210\266\345\216\237\347\220\206.md" "b/OperatingSystem/AndroidKernal/7.View\347\273\230\345\210\266\345\216\237\347\220\206.md" index a5def400..325c231f 100644 --- "a/OperatingSystem/AndroidKernal/7.View\347\273\230\345\210\266\345\216\237\347\220\206.md" +++ "b/OperatingSystem/AndroidKernal/7.View\347\273\230\345\210\266\345\216\237\347\220\206.md" @@ -2,11 +2,1624 @@ +界面窗口的根布局是`DecorView`,该类继承自`FrameLayout`.说到`View`绘制,想到的就是从这里入手,而`FrameLayout`继承自`ViewGroup`。感觉绘制肯定会在`ViewGroup`或者`View`中, +但是木有找到。发现`ViewGroup`实现`ViewParent`接口,而`ViewParent`有一个实现类是`ViewRootImpl`, `ViewGruop`中会使用`ViewRootImpl`... +```java +/** + * The top of a view hierarchy, implementing the needed protocol between View + * and the WindowManager. This is for the most part an internal implementation + * detail of {@link WindowManagerGlobal}. + * + * {@hide} + */ +@SuppressWarnings({"EmptyCatchBlock", "PointlessBooleanExpression"}) +public final class ViewRootImpl implements ViewParent, + View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks { + + } +``` +`View`的绘制过程从`ViewRootImpl.performTraversals()`方法开始,你看这个名字起的多好,叫执行遍历,看到名字就能知道内部的实现: +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/view_performTraversals.png) +首先先说明一下,这部分代码比较多,逻辑也比较麻烦,很容易弄晕,如果感觉看起来费劲,就跳过这一块,直接到下面的Measure、Layout、Draw部分开始看。 +我也没有全部弄清楚,我只是把里面的步骤标注了下。 +```java +private void performTraversals() { + // ... 此处省略源代码N行 + if (mFirst || windowShouldResize || insetsChanged || + viewVisibilityChanged || params != null || mForceNextWindowRelayout) { + // 第一或者resize等都会调用relayoutWindow,而该函数内部会调用sWindowSession.relayout() + // 方法来请求WmS按照指定的大小重新分配窗口大小,并会为客户窗口创建的mSurface对象分配真正的现存 + // 等该函数返回后,应用程序就可以在该Surface中绘制了。 + relayoutResult = relayoutWindow(params, viewVisibility, insetsPending); + } + // 是否需要Measure + if (!mStopped) { + boolean focusChangedDueToTouchMode = ensureTouchModeLocally( + (relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0); + if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth() + || mHeight != host.getMeasuredHeight() || contentInsetsChanged) { + // 这里是获取widthMeasureSpec,这俩参数不是一般的尺寸数值,而是将模式和尺寸组合在一起的数值. + // getRootMeasureSpec方法内部会使用MeasureSpec.makeMeasureSpec()方法来组装一个MeasureSpec, + // 当lp.width参数等于MATCH_PARENT的时候,MeasureSpec的specMode就等于EXACTLY,当lp.width等于WRAP_CONTENT的时候,MeasureSpec的specMode就等于AT_MOST。 + // 并且MATCH_PARENT和WRAP_CONTENT时的specSize都是等于windowSize的,也就意味着根视图总是会充满全屏的。 + // 这里lp代表的是根视图的LayoutParams、lp.width和lp.height直接来源于用户的定义比如WRAP_CONTENT、MATCH_PARENT等 + int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); + int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); + + if (DEBUG_LAYOUT) Log.v(TAG, "Ooops, something changed! mWidth=" + + mWidth + " measuredWidth=" + host.getMeasuredWidth() + + " mHeight=" + mHeight + + " measuredHeight=" + host.getMeasuredHeight() + + " coveredInsetsChanged=" + contentInsetsChanged); + + // 调用PerformMeasure方法。 + // Ask host how big it wants to be + performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); + + // Implementation of weights from WindowManager.LayoutParams + // We just grow the dimensions as needed and re-measure if + // needs be + int width = host.getMeasuredWidth(); + int height = host.getMeasuredHeight(); + boolean measureAgain = false; + + if (lp.horizontalWeight > 0.0f) { + width += (int) ((mWidth - width) * lp.horizontalWeight); + childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, + MeasureSpec.EXACTLY); + measureAgain = true; + } + if (lp.verticalWeight > 0.0f) { + height += (int) ((mHeight - height) * lp.verticalWeight); + childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, + MeasureSpec.EXACTLY); + measureAgain = true; + } + + if (measureAgain) { + if (DEBUG_LAYOUT) Log.v(TAG, + "And hey let's measure once more: width=" + width + + " height=" + height); + performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); + } + + layoutRequested = true; + } + } + + final boolean didLayout = layoutRequested && !mStopped; + boolean triggerGlobalLayoutListener = didLayout + || mAttachInfo.mRecomputeGlobalAttributes; + // 是否需要Layout + if (didLayout) { + // 调用performLayout方法。 + performLayout(lp, desiredWindowWidth, desiredWindowHeight); + + // By this point all views have been sized and positioned + // We can compute the transparent area + + if ((host.mPrivateFlags & View.PFLAG_REQUEST_TRANSPARENT_REGIONS) != 0) { + // start out transparent + // TODO: AVOID THAT CALL BY CACHING THE RESULT? + host.getLocationInWindow(mTmpLocation); + mTransparentRegion.set(mTmpLocation[0], mTmpLocation[1], + mTmpLocation[0] + host.mRight - host.mLeft, + mTmpLocation[1] + host.mBottom - host.mTop); + + host.gatherTransparentRegion(mTransparentRegion); + if (mTranslator != null) { + mTranslator.translateRegionInWindowToScreen(mTransparentRegion); + } + + if (!mTransparentRegion.equals(mPreviousTransparentRegion)) { + mPreviousTransparentRegion.set(mTransparentRegion); + mFullRedrawNeeded = true; + // reconfigure window manager + try { + mWindowSession.setTransparentRegion(mWindow, mTransparentRegion); + } catch (RemoteException e) { + } + } + } + + if (DBG) { + System.out.println("======================================"); + System.out.println("performTraversals -- after setFrame"); + host.debug(); + } + } + + // 是否需要Draw + if (!cancelDraw && !newSurface) { + if (!skipDraw || mReportNextDraw) { + if (mPendingTransitions != null && mPendingTransitions.size() > 0) { + for (int i = 0; i < mPendingTransitions.size(); ++i) { + mPendingTransitions.get(i).startChangingAnimations(); + } + mPendingTransitions.clear(); + } + // 调用performDraw方法 + performDraw(); + } + } else { + if (viewVisibility == View.VISIBLE) { + // Try again + scheduleTraversals(); + } else if (mPendingTransitions != null && mPendingTransitions.size() > 0) { + for (int i = 0; i < mPendingTransitions.size(); ++i) { + mPendingTransitions.get(i).endChangingAnimations(); + } + mPendingTransitions.clear(); + } + } + + mIsInTraversal = false; +} +``` + +从上面源码可以看出,`performTraversals()`方法中会依次做三件事: +- `performMeasure()`, 内部是` mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);`测量`View`大小。这里顺便提一下,这个`mView`是什么?它就是`Window`最顶成的`View(DecorView)`,它是`FrameLayout`的子类。 +- `performLayout()`, 内部是`mView.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());`视图布局,确定`View`位置。 +- `performDraw()`, 内部是`draw(fullRedrawNeeded);` 绘制界面。 + +至此`View`绘制的三个过程已经展现: + +`Measure` +=== + + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/view_measure.png) + + + +`performMeasure`方法如下: + +```java +private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) { + Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure"); + try { + mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIEW); + } +} +``` + +在`performMeasure()`方法中会调用`View.measure()`方法, 源码如下: +```java +/** + *

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

+ * + *

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

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

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

+ * + *

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

+ * + *

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

+ * + *

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

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

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

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

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

+ * + *

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

+ * + * @param l Left position, relative to parent + * @param t Top position, relative to parent + * @param r Right position, relative to parent + * @param b Bottom position, relative to parent + */ +@SuppressWarnings({"unchecked"}) +public void layout(int l, int t, int r, int b) { + if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) { + onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec); + mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; + } + + int oldL = mLeft; + int oldT = mTop; + int oldB = mBottom; + int oldR = mRight; + + // 这部分是判断这个View的大小是否已经发生了变化,来判断是否需要重绘。 + boolean changed = isLayoutModeOptical(mParent) ? + setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); + + if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { + // 内部调用onLayout方法 + onLayout(changed, l, t, r, b); + mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED; + + ListenerInfo li = mListenerInfo; + if (li != null && li.mOnLayoutChangeListeners != null) { + ArrayList listenersCopy = + (ArrayList)li.mOnLayoutChangeListeners.clone(); + int numListeners = listenersCopy.size(); + for (int i = 0; i < numListeners; ++i) { + listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB); + } + } + } + + mPrivateFlags &= ~PFLAG_FORCE_LAYOUT; + mPrivateFlags3 |= PFLAG3_IS_LAID_OUT; +} +``` +这里会调用`onLayout`方法,同样因为`mView`是`FrameLayout`的子类,所以我们要看`FrameLayout`的`onLayout`方法, +这里我们先看一下`ViewGroup.onLayout`方法: +```java +/** + * {@inheritDoc} + */ +@Override +protected abstract void onLayout(boolean changed, + int l, int t, int r, int b); +``` +是个抽象方法,所以`ViewGroup`的子类都需要实现该方法。 +我们看一下`FrameLayout.onLayout`方法,源码如下: +```java + /** + * {@inheritDoc} + */ +@Override +protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + layoutChildren(left, top, right, bottom, false /* no force left gravity */); +} + +void layoutChildren(int left, int top, int right, int bottom, + boolean forceLeftGravity) { + final int count = getChildCount(); + + final int parentLeft = getPaddingLeftWithForeground(); + final int parentRight = right - left - getPaddingRightWithForeground(); + + final int parentTop = getPaddingTopWithForeground(); + final int parentBottom = bottom - top - getPaddingBottomWithForeground(); + + mForegroundBoundsChanged = true; + + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + final int width = child.getMeasuredWidth(); + final int height = child.getMeasuredHeight(); + + int childLeft; + int childTop; + + int gravity = lp.gravity; + if (gravity == -1) { + gravity = DEFAULT_CHILD_GRAVITY; + } + + final int layoutDirection = getLayoutDirection(); + final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection); + final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; + + switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { + case Gravity.CENTER_HORIZONTAL: + childLeft = parentLeft + (parentRight - parentLeft - width) / 2 + + lp.leftMargin - lp.rightMargin; + break; + case Gravity.RIGHT: + if (!forceLeftGravity) { + childLeft = parentRight - width - lp.rightMargin; + break; + } + case Gravity.LEFT: + default: + childLeft = parentLeft + lp.leftMargin; + } + + switch (verticalGravity) { + case Gravity.TOP: + childTop = parentTop + lp.topMargin; + break; + case Gravity.CENTER_VERTICAL: + childTop = parentTop + (parentBottom - parentTop - height) / 2 + + lp.topMargin - lp.bottomMargin; + break; + case Gravity.BOTTOM: + childTop = parentBottom - height - lp.bottomMargin; + break; + default: + childTop = parentTop + lp.topMargin; + } + //调用子View的layout方法 + child.layout(childLeft, childTop, childLeft + width, childTop + height); + } + } +} +``` +而`View.layout`方法,又会调用到`View.onLayout`方法,我们假设这个子`View`不是`ViewGroup`. +看一下`View.onLayout`方法源码如下: +```java +/** + * Called from layout when this view should + * assign a size and position to each of its children. + * + * Derived classes with children should override + * this method and call layout on each of + * their children. + * @param changed This is a new size or position for this view + * @param left Left position, relative to parent + * @param top Top position, relative to parent + * @param right Right position, relative to parent + * @param bottom Bottom position, relative to parent + */ +protected void onLayout(boolean changed, int left, int top, int right, int bottom) { +} +``` +是一个空方法,这是因为`Layout`需要`ViewGroup`来控制进行。 + +这里也总结一下`layout`的过程。 +```java +private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, + int desiredWindowHeight) { + - 1. host.layout + host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); + -2. layout方法会分别调用setFrame()和onLayout()方法 + setFrame() + onLayout() + -3. 因为host是mView也就是DecorView也就是FrameLayout的子类。FrameLayout的onLayout方法如下 + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + -4. 遍历每个子View,并分别调用layout方法。 + child.layout(childLeft, childTop, childLeft + width, childTop + height); + } + } +} +``` + +`Draw` +=== + + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/view_draw.png) + +绘制阶段是从`ViewRootImpl`中的`performDraw`方法开始的: + +```java +private void performDraw() { + if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) { + return; + } + + final boolean fullRedrawNeeded = mFullRedrawNeeded; + mFullRedrawNeeded = false; + + mIsDrawing = true; + Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw"); + try { + // 开始draw了 + draw(fullRedrawNeeded); + } finally { + mIsDrawing = false; + Trace.traceEnd(Trace.TRACE_TAG_VIEW); + } + + // For whatever reason we didn't create a HardwareRenderer, end any + // hardware animations that are now dangling + if (mAttachInfo.mPendingAnimatingRenderNodes != null) { + final int count = mAttachInfo.mPendingAnimatingRenderNodes.size(); + for (int i = 0; i < count; i++) { + mAttachInfo.mPendingAnimatingRenderNodes.get(i).endAllAnimators(); + } + mAttachInfo.mPendingAnimatingRenderNodes.clear(); + } + + if (mReportNextDraw) { + mReportNextDraw = false; + if (mAttachInfo.mHardwareRenderer != null) { + mAttachInfo.mHardwareRenderer.fence(); + } + + if (LOCAL_LOGV) { + Log.v(TAG, "FINISHED DRAWING: " + mWindowAttributes.getTitle()); + } + if (mSurfaceHolder != null && mSurface.isValid()) { + mSurfaceHolderCallback.surfaceRedrawNeeded(mSurfaceHolder); + SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks(); + if (callbacks != null) { + for (SurfaceHolder.Callback c : callbacks) { + if (c instanceof SurfaceHolder.Callback2) { + ((SurfaceHolder.Callback2)c).surfaceRedrawNeeded( + mSurfaceHolder); + } + } + } + } + try { + mWindowSession.finishDrawing(mWindow); + } catch (RemoteException e) { + } + } +} +``` +内部会调用`draw`方法,`draw`方法源码如下: +```java +private void draw(boolean fullRedrawNeeded) { + Surface surface = mSurface; + // 首先检查surface是否有效,正常情况下都是有效的,除非WmS发生异常不能为该客户端分配有效的Surface + if (!surface.isValid()) { + return; + } + + if (DEBUG_FPS) { + trackFPS(); + } + + if (!sFirstDrawComplete) { + synchronized (sFirstDrawHandlers) { + sFirstDrawComplete = true; + final int count = sFirstDrawHandlers.size(); + for (int i = 0; i< count; i++) { + mHandler.post(sFirstDrawHandlers.get(i)); + } + } + } + + scrollToRectOrFocus(null, false); + + if (mAttachInfo.mViewScrollChanged) { + mAttachInfo.mViewScrollChanged = false; + mAttachInfo.mTreeObserver.dispatchOnScrollChanged(); + } + + boolean animating = mScroller != null && mScroller.computeScrollOffset(); + final int curScrollY; + if (animating) { + curScrollY = mScroller.getCurrY(); + } else { + curScrollY = mScrollY; + } + if (mCurScrollY != curScrollY) { + mCurScrollY = curScrollY; + fullRedrawNeeded = true; + } + + final float appScale = mAttachInfo.mApplicationScale; + final boolean scalingRequired = mAttachInfo.mScalingRequired; + + int resizeAlpha = 0; + if (mResizeBuffer != null) { + long deltaTime = SystemClock.uptimeMillis() - mResizeBufferStartTime; + if (deltaTime < mResizeBufferDuration) { + float amt = deltaTime/(float) mResizeBufferDuration; + amt = mResizeInterpolator.getInterpolation(amt); + animating = true; + resizeAlpha = 255 - (int)(amt*255); + } else { + disposeResizeBuffer(); + } + } + + final Rect dirty = mDirty; + // 判断该Surface是否有SurfaceHolder对象,如果有则意味着该Surface是应用程序创建的,因为所有的绘制操作应该由应用程序 + // 自身去负责,于是View系统推出绘制,如果不是,才开始View绘制的内部流程。 + if (mSurfaceHolder != null) { + // The app owns the surface, we won't draw. + dirty.setEmpty(); + if (animating) { + if (mScroller != null) { + mScroller.abortAnimation(); + } + disposeResizeBuffer(); + } + return; + } + + if (fullRedrawNeeded) { + mAttachInfo.mIgnoreDirtyState = true; + dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f)); + } + + if (DEBUG_ORIENTATION || DEBUG_DRAW) { + Log.v(TAG, "Draw " + mView + "/" + + mWindowAttributes.getTitle() + + ": dirty={" + dirty.left + "," + dirty.top + + "," + dirty.right + "," + dirty.bottom + "} surface=" + + surface + " surface.isValid()=" + surface.isValid() + ", appScale:" + + appScale + ", width=" + mWidth + ", height=" + mHeight); + } + + mAttachInfo.mTreeObserver.dispatchOnDraw(); + + int xOffset = 0; + int yOffset = curScrollY; + final WindowManager.LayoutParams params = mWindowAttributes; + final Rect surfaceInsets = params != null ? params.surfaceInsets : null; + if (surfaceInsets != null) { + xOffset -= surfaceInsets.left; + yOffset -= surfaceInsets.top; + + // Offset dirty rect for surface insets. + dirty.offset(surfaceInsets.left, surfaceInsets.right); + } + + if (!dirty.isEmpty() || mIsAnimating) { + // Surface的底层驱动模式分为两种,一种是使用图形加速支持的Surface,俗称显卡,另一种是使用CPU及内存模拟的Surface。 + // 因此这里需要根据不同的模式,进行不同的操作 + if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) { + // 硬件绘制 + // Draw with hardware renderer. + mIsAnimating = false; + boolean invalidateRoot = false; + if (mHardwareYOffset != yOffset || mHardwareXOffset != xOffset) { + mHardwareYOffset = yOffset; + mHardwareXOffset = xOffset; + mAttachInfo.mHardwareRenderer.invalidateRoot(); + } + mResizeAlpha = resizeAlpha; + + dirty.setEmpty(); + + mBlockResizeBuffer = false; + mAttachInfo.mHardwareRenderer.draw(mView, mAttachInfo, this); + } else { + // If we get here with a disabled & requested hardware renderer, something went + // wrong (an invalidate posted right before we destroyed the hardware surface + // for instance) so we should just bail out. Locking the surface with software + // rendering at this point would lock it forever and prevent hardware renderer + // from doing its job when it comes back. + // Before we request a new frame we must however attempt to reinitiliaze the + // hardware renderer if it's in requested state. This would happen after an + // eglTerminate() for instance. + if (mAttachInfo.mHardwareRenderer != null && + !mAttachInfo.mHardwareRenderer.isEnabled() && + mAttachInfo.mHardwareRenderer.isRequested()) { + + try { + mAttachInfo.mHardwareRenderer.initializeIfNeeded( + mWidth, mHeight, mSurface, surfaceInsets); + } catch (OutOfResourcesException e) { + handleOutOfResourcesException(e); + return; + } + + mFullRedrawNeeded = true; + scheduleTraversals(); + return; + } + + // 软件绘制 + if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) { + return; + } + } + } + + if (animating) { + mFullRedrawNeeded = true; + // 动画就是让画面动起来,如果正在动画过程中,则需要再次发起一个重绘命令,以便接着绘制,直到滚动结束。 + scheduleTraversals(); + } +} +``` +我们看一下`drawSoftware`方法: +```java +/** + * 使用CPU的软件绘制方式 + * @return true if drawing was successful, false if an error occurred + */ +private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, + boolean scalingRequired, Rect dirty) { + + // Draw with software renderer. + final Canvas canvas; + try { + final int left = dirty.left; + final int top = dirty.top; + final int right = dirty.right; + final int bottom = dirty.bottom; + + canvas = mSurface.lockCanvas(dirty); + + // The dirty rectangle can be modified by Surface.lockCanvas() + //noinspection ConstantConditions + if (left != dirty.left || top != dirty.top || right != dirty.right + || bottom != dirty.bottom) { + attachInfo.mIgnoreDirtyState = true; + } + + // TODO: Do this in native + canvas.setDensity(mDensity); + } catch (Surface.OutOfResourcesException e) { + handleOutOfResourcesException(e); + return false; + } catch (IllegalArgumentException e) { + Log.e(TAG, "Could not lock surface", e); + // Don't assume this is due to out of memory, it could be + // something else, and if it is something else then we could + // kill stuff (or ourself) for no reason. + mLayoutRequested = true; // ask wm for a new surface next time. + return false; + } + + try { + if (DEBUG_ORIENTATION || DEBUG_DRAW) { + Log.v(TAG, "Surface " + surface + " drawing to bitmap w=" + + canvas.getWidth() + ", h=" + canvas.getHeight()); + //canvas.drawARGB(255, 255, 0, 0); + } + + // If this bitmap's format includes an alpha channel, we + // need to clear it before drawing so that the child will + // properly re-composite its drawing on a transparent + // background. This automatically respects the clip/dirty region + // or + // If we are applying an offset, we need to clear the area + // where the offset doesn't appear to avoid having garbage + // left in the blank areas. + if (!canvas.isOpaque() || yoff != 0 || xoff != 0) { + canvas.drawColor(0, PorterDuff.Mode.CLEAR); + } + + dirty.setEmpty(); + mIsAnimating = false; + attachInfo.mDrawingTime = SystemClock.uptimeMillis(); + mView.mPrivateFlags |= View.PFLAG_DRAWN; + + if (DEBUG_DRAW) { + Context cxt = mView.getContext(); + Log.i(TAG, "Drawing: package:" + cxt.getPackageName() + + ", metrics=" + cxt.getResources().getDisplayMetrics() + + ", compatibilityInfo=" + cxt.getResources().getCompatibilityInfo()); + } + try { + canvas.translate(-xoff, -yoff); + if (mTranslator != null) { + mTranslator.translateCanvas(canvas); + } + canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0); + attachInfo.mSetIgnoreDirtyState = false; + + // 内部会去调用View.draw(); + mView.draw(canvas); + } finally { + if (!attachInfo.mSetIgnoreDirtyState) { + // Only clear the flag if it was not set during the mView.draw() call + attachInfo.mIgnoreDirtyState = false; + } + } + } finally { + try { + surface.unlockCanvasAndPost(canvas); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Could not unlock surface", e); + mLayoutRequested = true; // ask wm for a new surface next time. + //noinspection ReturnInsideFinallyBlock + return false; + } + + if (LOCAL_LOGV) { + Log.v(TAG, "Surface " + surface + " unlockCanvasAndPost"); + } + } + return true; +} +``` +代码中调用了`mView.draw()`方法,所以我们看一下`FrameLayout.draw()`方法: +```java +/** + * {@inheritDoc} + */ +@Override +public void draw(Canvas canvas) { + super.draw(canvas); + + if (mForeground != null) { + final Drawable foreground = mForeground; + + if (mForegroundBoundsChanged) { + mForegroundBoundsChanged = false; + final Rect selfBounds = mSelfBounds; + final Rect overlayBounds = mOverlayBounds; + + final int w = mRight-mLeft; + final int h = mBottom-mTop; + + if (mForegroundInPadding) { + selfBounds.set(0, 0, w, h); + } else { + selfBounds.set(mPaddingLeft, mPaddingTop, w - mPaddingRight, h - mPaddingBottom); + } + + final int layoutDirection = getLayoutDirection(); + Gravity.apply(mForegroundGravity, foreground.getIntrinsicWidth(), + foreground.getIntrinsicHeight(), selfBounds, overlayBounds, + layoutDirection); + foreground.setBounds(overlayBounds); + } + + foreground.draw(canvas); + } +} +``` +内部调用了`super.draw()`,而`ViewGroup`没有重写该方法,所以直接看`View`的`draw()`方法. +`View.draw()`方法如下: +```java +/** + * Manually render this view (and all of its children) to the given Canvas. + * The view must have already done a full layout before this function is + * called. When implementing a view, implement + * {@link #onDraw(android.graphics.Canvas)} instead of overriding this method. + * If you do need to override this method, call the superclass version. + * + * @param canvas The Canvas to which the View is rendered. + */ +public void draw(Canvas canvas) { + final int privateFlags = mPrivateFlags; + final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE && + (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState); + mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN; + + // 这里注释说的很明白了,draw的6个步骤。 + /* + * Draw traversal performs several drawing steps which must be executed + * in the appropriate order: + * + * 1. Draw the background + * 2. If necessary, save the canvas' layers to prepare for fading + * 3. Draw view's content, 调用onDraw方法绘制自身 + * 4. Draw children, 调用dispatchDraw方法绘制子View + * 5. If necessary, draw the fading edges and restore layers + * 6. Draw decorations (scrollbars for instance) + */ + + // Step 1, draw the background, if needed + int saveCount; + + if (!dirtyOpaque) { + drawBackground(canvas); + } + + // skip step 2 & 5 if possible (common case) + final int viewFlags = mViewFlags; + boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0; + boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0; + if (!verticalEdges && !horizontalEdges) { + // Step 3, draw the content + if (!dirtyOpaque) onDraw(canvas); + + // Step 4, draw the children + dispatchDraw(canvas); + + // Step 6, draw decorations (scrollbars) + onDrawScrollBars(canvas); + + if (mOverlay != null && !mOverlay.isEmpty()) { + mOverlay.getOverlayView().dispatchDraw(canvas); + } + + // we're done... + return; + } + + /* + * Here we do the full fledged routine... + * (this is an uncommon case where speed matters less, + * this is why we repeat some of the tests that have been + * done above) + */ + + boolean drawTop = false; + boolean drawBottom = false; + boolean drawLeft = false; + boolean drawRight = false; + + float topFadeStrength = 0.0f; + float bottomFadeStrength = 0.0f; + float leftFadeStrength = 0.0f; + float rightFadeStrength = 0.0f; + + // Step 2, save the canvas' layers + int paddingLeft = mPaddingLeft; + + final boolean offsetRequired = isPaddingOffsetRequired(); + if (offsetRequired) { + paddingLeft += getLeftPaddingOffset(); + } + + int left = mScrollX + paddingLeft; + int right = left + mRight - mLeft - mPaddingRight - paddingLeft; + int top = mScrollY + getFadeTop(offsetRequired); + int bottom = top + getFadeHeight(offsetRequired); + + if (offsetRequired) { + right += getRightPaddingOffset(); + bottom += getBottomPaddingOffset(); + } + + final ScrollabilityCache scrollabilityCache = mScrollCache; + final float fadeHeight = scrollabilityCache.fadingEdgeLength; + int length = (int) fadeHeight; + + // clip the fade length if top and bottom fades overlap + // overlapping fades produce odd-looking artifacts + if (verticalEdges && (top + length > bottom - length)) { + length = (bottom - top) / 2; + } + + // also clip horizontal fades if necessary + if (horizontalEdges && (left + length > right - length)) { + length = (right - left) / 2; + } + + if (verticalEdges) { + topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength())); + drawTop = topFadeStrength * fadeHeight > 1.0f; + bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength())); + drawBottom = bottomFadeStrength * fadeHeight > 1.0f; + } + + if (horizontalEdges) { + leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength())); + drawLeft = leftFadeStrength * fadeHeight > 1.0f; + rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength())); + drawRight = rightFadeStrength * fadeHeight > 1.0f; + } + + saveCount = canvas.getSaveCount(); + + int solidColor = getSolidColor(); + if (solidColor == 0) { + final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG; + + if (drawTop) { + canvas.saveLayer(left, top, right, top + length, null, flags); + } + + if (drawBottom) { + canvas.saveLayer(left, bottom - length, right, bottom, null, flags); + } + + if (drawLeft) { + canvas.saveLayer(left, top, left + length, bottom, null, flags); + } + + if (drawRight) { + canvas.saveLayer(right - length, top, right, bottom, null, flags); + } + } else { + scrollabilityCache.setFadeColor(solidColor); + } + + // Step 3, draw the content + if (!dirtyOpaque) onDraw(canvas); + + // Step 4, draw the children + dispatchDraw(canvas); + + // Step 5, draw the fade effect and restore layers + final Paint p = scrollabilityCache.paint; + final Matrix matrix = scrollabilityCache.matrix; + final Shader fade = scrollabilityCache.shader; + + if (drawTop) { + matrix.setScale(1, fadeHeight * topFadeStrength); + matrix.postTranslate(left, top); + fade.setLocalMatrix(matrix); + p.setShader(fade); + canvas.drawRect(left, top, right, top + length, p); + } + + if (drawBottom) { + matrix.setScale(1, fadeHeight * bottomFadeStrength); + matrix.postRotate(180); + matrix.postTranslate(left, bottom); + fade.setLocalMatrix(matrix); + p.setShader(fade); + canvas.drawRect(left, bottom - length, right, bottom, p); + } + + if (drawLeft) { + matrix.setScale(1, fadeHeight * leftFadeStrength); + matrix.postRotate(-90); + matrix.postTranslate(left, top); + fade.setLocalMatrix(matrix); + p.setShader(fade); + canvas.drawRect(left, top, left + length, bottom, p); + } + + if (drawRight) { + matrix.setScale(1, fadeHeight * rightFadeStrength); + matrix.postRotate(90); + matrix.postTranslate(right, top); + fade.setLocalMatrix(matrix); + p.setShader(fade); + canvas.drawRect(right - length, top, right, bottom, p); + } + + canvas.restoreToCount(saveCount); + + // Step 6, draw decorations (scrollbars) + onDrawScrollBars(canvas); + + if (mOverlay != null && !mOverlay.isEmpty()) { + mOverlay.getOverlayView().dispatchDraw(canvas); + } +} +``` + +上面会调用`onDraw`和`dispatchDraw`方法。 +我们先看一下`View.onDraw`方法: +```java +/** + * Implement this to do your drawing. + * + * @param canvas the canvas on which the background will be drawn + */ +protected void onDraw(Canvas canvas) { +} +``` +是空方法,这是也很好理解,因为每个`View`的展现都不一样,例如`TextView`、`ProgressBar`等, +所以`View`不会去实现`onDraw`方法,具体是要子类去根据自己的显示要求实现该方法。 + +再看一下`dispatchDraw`方法,这个方法是用来绘制子`View`的,所以要看`ViewGroup.dispatchDraw`方法,`View.dispatchDraw`是空的。 +```java +/** + * {@inheritDoc} + */ +@Override +protected void dispatchDraw(Canvas canvas) { + boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode); + final int childrenCount = mChildrenCount; + final View[] children = mChildren; + int flags = mGroupFlags; + + if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) { + final boolean cache = (mGroupFlags & FLAG_ANIMATION_CACHE) == FLAG_ANIMATION_CACHE; + + final boolean buildCache = !isHardwareAccelerated(); + for (int i = 0; i < childrenCount; i++) { + final View child = children[i]; + if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) { + final LayoutParams params = child.getLayoutParams(); + attachLayoutAnimationParameters(child, params, i, childrenCount); + bindLayoutAnimation(child); + if (cache) { + child.setDrawingCacheEnabled(true); + if (buildCache) { + child.buildDrawingCache(true); + } + } + } + } + + final LayoutAnimationController controller = mLayoutAnimationController; + if (controller.willOverlap()) { + mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE; + } + + controller.start(); + + mGroupFlags &= ~FLAG_RUN_ANIMATION; + mGroupFlags &= ~FLAG_ANIMATION_DONE; + + if (cache) { + mGroupFlags |= FLAG_CHILDREN_DRAWN_WITH_CACHE; + } + + if (mAnimationListener != null) { + mAnimationListener.onAnimationStart(controller.getAnimation()); + } + } + + int clipSaveCount = 0; + final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK; + if (clipToPadding) { + clipSaveCount = canvas.save(); + canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop, + mScrollX + mRight - mLeft - mPaddingRight, + mScrollY + mBottom - mTop - mPaddingBottom); + } + + // We will draw our child's animation, let's reset the flag + mPrivateFlags &= ~PFLAG_DRAW_ANIMATION; + mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED; + + boolean more = false; + final long drawingTime = getDrawingTime(); + + if (usingRenderNodeProperties) canvas.insertReorderBarrier(); + // Only use the preordered list if not HW accelerated, since the HW pipeline will do the + // draw reordering internally + final ArrayList preorderedList = usingRenderNodeProperties + ? null : buildOrderedChildList(); + final boolean customOrder = preorderedList == null + && isChildrenDrawingOrderEnabled(); + for (int i = 0; i < childrenCount; i++) { + int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i; + final View child = (preorderedList == null) + ? children[childIndex] : preorderedList.get(childIndex); + if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { + // 调用drawChild方法 + more |= drawChild(canvas, child, drawingTime); + } + } + if (preorderedList != null) preorderedList.clear(); + + // Draw any disappearing views that have animations + if (mDisappearingChildren != null) { + final ArrayList disappearingChildren = mDisappearingChildren; + final int disappearingCount = disappearingChildren.size() - 1; + // Go backwards -- we may delete as animations finish + for (int i = disappearingCount; i >= 0; i--) { + final View child = disappearingChildren.get(i); + more |= drawChild(canvas, child, drawingTime); + } + } + if (usingRenderNodeProperties) canvas.insertInorderBarrier(); + + if (debugDraw()) { + onDebugDraw(canvas); + } + + if (clipToPadding) { + canvas.restoreToCount(clipSaveCount); + } + + // mGroupFlags might have been updated by drawChild() + flags = mGroupFlags; + + if ((flags & FLAG_INVALIDATE_REQUIRED) == FLAG_INVALIDATE_REQUIRED) { + invalidate(true); + } + + if ((flags & FLAG_ANIMATION_DONE) == 0 && (flags & FLAG_NOTIFY_ANIMATION_LISTENER) == 0 && + mLayoutAnimationController.isDone() && !more) { + // We want to erase the drawing cache and notify the listener after the + // next frame is drawn because one extra invalidate() is caused by + // drawChild() after the animation is over + mGroupFlags |= FLAG_NOTIFY_ANIMATION_LISTENER; + final Runnable end = new Runnable() { + public void run() { + notifyAnimationListener(); + } + }; + post(end); + } +} +``` +可以看到上面的方法中会调用`drawChild`方法,该方法如下: +```java +/** + * Draw one child of this View Group. This method is responsible for getting + * the canvas in the right state. This includes clipping, translating so + * that the child's scrolled origin is at 0, 0, and applying any animation + * transformations. + * + * @param canvas The canvas on which to draw the child + * @param child Who to draw + * @param drawingTime The time at which draw is occurring + * @return True if an invalidate() was issued + */ +protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + return child.draw(canvas, this, drawingTime); +} +``` + +这里也简单总结一下`draw`的过程: +```java +// 1. ViewRootImpl.performDraw() +private void performDraw() { + // 2. ViewRootImpl.draw() + draw(fullRedrawNeeded); + // 3. ViewRootImpl.drawSoftware + drawSoftware + // 4. 内部调用mView.draw,也就是FrameLayout.draw(). + mView.draw()(FrameLayout) + // 5. FrameLayout.draw方法内部会调用super.draw方法,也就是View.draw方法. + super.draw(canvas); + // 6. View.draw方法内部会分别调用onDraw绘制自己以及dispatchDraw绘制子View. + onDraw + // 绘制子View + dispatchDraw + // 7. dispatchDraw方法内部会遍历所有子View. + for (int i = 0; i < childrenCount; i++) { + // 8. 对每个子View分别调用drawChild方法 + drawChild() + // 9. drawChild方法内部会对该子View调用draw方法,进行绘制。然后draw又会调用onDraw等,循环就开始了。 + child.draw() + } +} +``` + +最后补充一个小问题: `getWidth()`与`getMeasuredWidth()`有什么区别呢? +一般情况下这两个的值是相同的,`getMeasureWidth()`方法在`measure()`过程结束后就可以获取到了,而`getWidth()`方法要在`layout()`过程结束后才能获取到。 +而且`getMeasureWidth()`的值是通过`setMeasuredDimension()`设置的,但是`getWidth()`的值是通过视图右边的坐标减去左边的坐标计算出来的。如果我们在`layout`的时候将宽高 +不传`getMeasureWidth`的值,那么这时候`getWidth()`与`getMeasuredWidth`的值就不会再相同了,当然一般也不会这么干... + +# MeasureSpec + +MeasureSpec是View类的一个静态内部类,用来说明如何测量这个类。MeasureSpec表示的是一个32位的整型值,它的高2位表示测量模式SpecMode,低30位表示某种测量模式下的规格大小SpecSize。MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的内存分配。 + +--- + +- [上一篇:6.屏幕绘制基础](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/6.%E5%B1%8F%E5%B9%95%E7%BB%98%E5%88%B6%E5%9F%BA%E7%A1%80.md) +- [下一篇:8.WindowManagerService简介](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/8.WindowManagerService%E7%AE%80%E4%BB%8B.md) diff --git "a/OperatingSystem/AndroidKernal/8.WindowManagerService\347\256\200\344\273\213.md" "b/OperatingSystem/AndroidKernal/8.WindowManagerService\347\256\200\344\273\213.md" index 7fa274dc..cb3820e1 100644 --- "a/OperatingSystem/AndroidKernal/8.WindowManagerService\347\256\200\344\273\213.md" +++ "b/OperatingSystem/AndroidKernal/8.WindowManagerService\347\256\200\344\273\213.md" @@ -1,7 +1,7 @@ # 8.WindowManagerService简介 -WmS是Android中图形用户接口的引擎,它管理者所有窗口,包括创建、删除窗口以及将某个窗口设置为焦点等。 +WmS是Android中图形用户接口的引擎,它管理着所有窗口,包括创建、删除窗口以及将某个窗口设置为焦点等。 在WmS中,窗口是由两部分内容构成: @@ -63,7 +63,7 @@ WmS接口结构是指WmS功能模块与其他模块之间的交互接口,其 ### Session -和SurfaceFlinger直接打交道的类本来是SurfaceSession。当应用程序需要创建Surface时,会请求WmS去完成创建的工作,WmS回味每一个应用程序分配一个SurfaceSession对象。然而一个surfaceSession对象不足以表示一个客户端,因此,WmS定义了Session类,它可被认为是SurfaceSession的一个包装。Session对象是当应用程序调用WmS的openSession()函数时创建的,而应用程序又是在ViewRoot类中调用openSession的。 +和SurfaceFlinger直接打交道的类本来是SurfaceSession。当应用程序需要创建Surface时,会请求WmS去完成创建的工作,WmS会为每一个应用程序分配一个SurfaceSession对象。然而一个surfaceSession对象不足以表示一个客户端,因此,WmS定义了Session类,它可被认为是SurfaceSession的一个包装。Session对象是当应用程序调用WmS的openSession()函数时创建的,而应用程序又是在ViewRoot类中调用openSession的。 @@ -122,6 +122,11 @@ Surface的销毁有两种情况: +--- + +- [上一篇:7.View绘制原理](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/7.View%E7%BB%98%E5%88%B6%E5%8E%9F%E7%90%86.md) +- [下一篇:9.PackageManagerService简介](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/9.PackageManagerService%E7%AE%80%E4%BB%8B.md) + diff --git "a/OperatingSystem/AndroidKernal/9.PackageManagerService\347\256\200\344\273\213.md" "b/OperatingSystem/AndroidKernal/9.PackageManagerService\347\256\200\344\273\213.md" index 803a99df..83a469b5 100644 --- "a/OperatingSystem/AndroidKernal/9.PackageManagerService\347\256\200\344\273\213.md" +++ "b/OperatingSystem/AndroidKernal/9.PackageManagerService\347\256\200\344\273\213.md" @@ -20,7 +20,7 @@ 和AmS、WmS等其他系统服务一样,包管理服务运行于SystemServer进程。PmS服务运行时,使用了两个目录下的XML文件保存相关的包管理信息。 - - 第一个目录是system/etc/permissions:该目录下的所有xml文件用于permission的管理,具体包含两件时间。第一个是定义系统中都包含了哪些feature,应用程序可以在AndroidManifest.xml中使用use-feature标签声明程序都需要哪些feature。 + - 第一个目录是system/etc/permissions:该目录下的所有xml文件用于permission的管理,具体包含两个事件。第一个是定义系统中都包含了哪些feature,应用程序可以在AndroidManifest.xml中使用use-feature标签声明程序都需要哪些feature。 - 第二个目录是/data/system/packages.xml,该文件保存了所有安装程序的基本包信息,有点像系统的注册表,比如程序的包名称是什么,安装包路径在哪里,程序都是用了哪些系统权限。 PmS在启动时,会从这两个目录中解析相关的XML文件,从而建立一个庞大的包信息树,应用程序可以间接从这个信息树中查询所有所需的程序包信息。 @@ -58,6 +58,14 @@ + +--- + +- [上一篇:8.WindowManagerService简介](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/8.WindowManagerService%E7%AE%80%E4%BB%8B.md) + + + + --- - 邮箱 :charon.chui@gmail.com From ddd5aaecaf8526f0fd3ab3c02a128b19b2a41a5c Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 9 Dec 2020 20:26:16 +0800 Subject: [PATCH 041/213] add files --- ...73\347\273\237\347\256\200\344\273\213.md" | 8 +- ...13\344\270\216\347\272\277\347\250\213.md" | 4 + ...13\351\227\264\351\200\232\344\277\241.md" | 283 ++++++++++++++---- ...roid Framework\346\241\206\346\236\266.md" | 17 +- ...72\347\241\200\347\237\245\350\257\206.md" | 2 +- 5 files changed, 257 insertions(+), 57 deletions(-) rename "VideoDevelopment/\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" => "VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/1.\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" (97%) diff --git "a/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" index 4e34b27f..82ba0f2a 100644 --- "a/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" +++ "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" @@ -134,7 +134,7 @@ 目前的Android系统大多运行在ARM处理器之上。ARM本身是一个公司的名称,从技术的角度来看,它又是一种微处理器内核的架构。 -对于ARM处理器,当复位完毕后,处理器首先还行其片上ROM中的一小块程序。这块ROM的大小一般只有几KB,改段程序就是Bootloader程序,这段程序执行时会根据处理器上一些特定引脚的高低电平状态,选择从何种物理接口上装载用户程序,比如USB口、SD卡、并口Flash等。 +对于ARM处理器,当复位完毕后,处理器首先还行其片上ROM中的一小块程序。这块ROM的大小一般只有几KB,该段程序就是Bootloader程序,这段程序执行时会根据处理器上一些特定引脚的高低电平状态,选择从何种物理接口上装载用户程序,比如USB口、SD卡、并口Flash等。 多数基于ARM的实际硬件系统,会从并口NAND Flash芯片上的0x00000000地址处装载程序。对于一些小型嵌入式系统而言,该地址中的程序就是最终要执行的用户程序;而对于Android而言,该地址中的程序还不是Android程序,而是一个叫做uboot或者fastboot的程序,其作用是初始化硬件设备,比如网口、SDRAM、RS232等,并提供一些调试功能,比如向NAND Flash中写入新的数据,这可用于开发过程中的内核烧写、升级等。 @@ -146,6 +146,12 @@ Linux内核被装载后,就开始进行内核初始化的过程。 +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_system_start.png) + + + + + ## CPU(Central Processing Unit) CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从内存中提取指令并执行它。它是一块超大规模的集成电路(Integrated Circuit),是信息处理、程序运行的最终执行单元。其功能主要是解释计算机指令以及处理计算机软件中的数据。由于访问内存获取执行或数据要比执行指令花费的时间长,因此所有的 CPU 内部都会包含一些寄存器来保存关键变量和临时结果。因此,在指令集中通常会有一些指令用于把关键字从内存中加载到寄存器中,以及把关键字从寄存器存入到内存中。从功能方面来看,CPU的内部主要由寄存器,控制器,运算器构成,各部分之间由电流信号相互连通。其中运算器负责算术运算和逻辑运算,控制器负责计算指令的解析,产生各种控制指令,寄存器组用来临时存放参加运算的数据和计算的中间结果。CPU计算结果最终需要写到内存中,内存的存取速度远低于CPU,为提升数据交换速率,CPU内部一般还集成了高速缓存(CACHE),其中缓存分为一级缓存和二级缓存,一级缓存和CPU速率相当,二级缓存次之。 diff --git "a/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" index 3f5369e6..8dd5f952 100644 --- "a/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" +++ "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" @@ -324,7 +324,11 @@ Linux为进程间通信和同步提供了各种机制。这里只是几种。 应用程序通过调用操作系统提供的库与操作系统进行交互,这些库合起来构成Android框架(Android framework)。这些库中有一些可以在进程内部执行其工作,但是许多库需要与其他进程执行进程间通信,作者通常是在system_server进程中提供服务的。 +### ServiceManager +Android Binder的管理服务。 + +对于Binder驱动而言,**ServiceManager是一个守护进程,更是Android系统各个服务的管理者**。Android系统中的各个服务,都是添加到ServiceManager中进行管理的,而且每个服务都对应一个服务名。当Client获取某个服务时,则通过服务名来从ServiceManager中获取相应的服务。 ### Zygote diff --git "a/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" "b/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" index 7263e837..2f089353 100644 --- "a/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" +++ "b/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" @@ -1,26 +1,52 @@ # 1.Android进程间通信 + + ## Binder简介 Binder,英文的意思是别针、回形针。我们经常用别针把两张纸”别“在一起,而在Android中,Binder用于完成进程间通信(IPC),即把多个进程”别“在一起。比如,普通应用程序可以调用音乐播放服务提供的播放、暂停、停止等功能。Binder工作在Linux层面,属于一个驱动,只是这个驱动不需要硬件,或者说其操作的硬件是基于一小段内存。从线程的角度来讲,Binder驱动代码运行在内核态,客户端程序调用Binder是通过系统调用完成的。 -## Linux进程间通信 +## 进程间通信 无论是Android系统,还是各种Linux衍生系统,各个组件、模块往往运行在各种不同的进程和线程内,这里就必然涉及进程/线程之间的通信。对于IPC(Inter-Process Communication, 进程间通信),Linux目前有一下这些IPC机制: -- 管道:在创建时分配一个page大小的内存,缓存区大小比较有限; -- 消息队列:信息会复制两次,会有额外的CPU消耗;不合适频繁或信息量大的通信; -- 共享内存:无须复制,共享缓冲区直接附加到进程虚拟地址空间,速度快;但进程间的同步问题操作系统无法实现,必须各进程利用同步工具解决; -- 套接字:作为更通用的接口,传输效率低,主要用于不同机器或跨网络的通信; -- 信号量:常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。 -- 信号: 不适用于信息交换,更适用于进程中断控制,比如非法内存访问,杀死某个进程等; +- 管道(Pipe) + + 管道是由内核管理的一个缓冲区,相当于我们放入内存中的一个纸条。管道的一端连接一个进程的输出。这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。 + + - 管道是半双工的,数据只能向一个方向流动。需要双方通信时,需要建立起两个管道。 + - 只能用于父子进程或兄弟进程之间(具有亲缘关系的进程)。比如fork或exec创建的新进程,在使用exec创建新进程时,需要将管道的文件描述符作为参数传递给exec创建的新进程。当父进程与使用fork创建的子进程直接通信时,发送数据的进程关闭读端,接受数据的进程关闭写端。 + - 管道只能在本地计算机中使用,而不可用于网络间的通信。 + +- 命名管道(FIFO) + + 命名管道是一种特殊类型的文件,它在系统中以文件形式存在。这样克服了管道的弊端,他可以允许没有亲缘关系的进程间通信。 + +- 共享内存(Share Memory) + + 共享内存是多个进程之间共享内存区域的一种进程间的通信方式,由IPC为进程创建一个特殊地址范围,它将出现在该进程的地址空间中。其他进程可以将同一段共享内存连接到自己的地址空间中。所有进程都可以访问共享内存的地址,如果一个进程向共享内存中写入数据,所做的改动将立刻被其他进程看到。 + + - 共享内存是IPC最快捷的方式,共享内存方式直接将某段内存段进行映射,多个进程间的共享内存是同一块的物理空间,仅仅映射到各进程的地址不同而已,因此不需要进行复制,可以直接使用此段空间。 + - 共享内存本身并没有同步机制,需要程序员自己控制。 + +- 内存映射(Memory Map) + + 内存映射是由一个文件到一块内存的映射,在此之后进程操作文件,就像操作进程空间里的内存地址一样。 + +- 套接字(Soket) + + 套接字机制不但可以在单机的不同进程间通信,而且可以在跨网机器间进程通信。 + + 套接字的创建和使用与管道是有区别的,套接字明确的将客户端与服务器区分开来,可以实现多个客户端连到同一个服务器。 Android额外还有Binder IPC机制,Android OS中的Zygote进程的IPC采用的是Socket机制,在上层system server、media server以及上层App之间更多的是采用Binder。 IPC方式来完成跨进程间的通信。对于Android上层架构中,很多时候是在同一个进程的线程之间需要相互通信,例如同一个进程的主线程与工作线程之间的通信,往往采用的Handler消息机制。所以对于Android最上层架构而言,最常用的通信方式是: - Binder + Android中最常用的跨进程通信方式。 + - Socket Socket通信方式也是C/S架构,比Binder简单很多。在Android系统中采用Socket通信方式的主要有: @@ -32,11 +58,62 @@ Android额外还有Binder IPC机制,Android OS中的Zygote进程的IPC采用 - logcatd:这个不用说,用于服务logcat; - vold:即volume Daemon,是存储类的守护进程,用于负责如USB、Sdcard等存储设备的事件处理。 - 等等还有很多,这里不一一列举,Socket方式更多的用于Android framework层与native层之间的通信。Socket通信方式相对于binder比较简单,这里省略。 + 等等还有很多,这里不一一列举,Socket方式更多的用于Android framework层与native层之间的通信。 - Handler - + 主要用于线程之间的通信。 + + + +### 进程隔离 + +进程隔离是操作系统为了保护进程之间不相互干扰而设计的,避免进程A写入进程B的情况发生,其实现就是使用虚拟地址空间,两个进程虚拟地址不同,这样就可以防止A进程写入数据到B进程。也就是说,操作系统的不同进程之间,数据不共享,在每个进程看来,自己都独享了整个系统空间,完全不知道其他进程的存在,因此一个进程想要与另一个进程通信,需要某种系统机制才能完成。 + + + +### 用户空间/内核空间 + +Linux Kernel是操作系统的核心,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。 + +对于Kernel这中高安全级别的功能,显然是不允许其它应用程序随便调用或访问的,所以需要对Kernel提供一定的保护机制,这个保护机制用来告诉那些应用程序,你只可以访问某些许可的资源,不许可的资源是不能访问的,于是操作系统就把kernel和上层的应用程序抽象的隔离开,分别称之为kenel space和user space,即内核空间和用户空间。 + + + +### 系统调用 内核态/用户态 + +虽然从逻辑上抽离出用户空间和内核空间,但是不可避免的是,总有那么一些用户空间需要访问内核的资源:比如应用程序访问文件、网络这种,那么这种情况下该怎么处理? + +用户空间访问内核空间的唯一方式就是系统调用,通过这个统一入口,所有的资源访问都是在内核的控制下执行,以免导致用户程序对系统资源的越权访问,从而保障了系统的安全和稳定。 + +Linux 使用两级保护机制:0 级供系统内核使用,3 级供用户程序使用。 + +当一个任务(进程)执行系统调用而陷入内核代码中执行时,称进程处于内核运行态(内核态)。此时处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。 + +当进程在执行用户自己的代码的时候,我们称其处于用户运行态(用户态)。此时处理器在特权级最低的(3级)用户代码中运行。 + +系统调用主要通过如下两个函数来实现: + +``` +copy_from_user() //将数据从用户空间拷贝到内核空间 +copy_to_user() //将数据从内核空间拷贝到用户空间 +``` + + + +### 内核模块/驱动 + +通过系统调用,用户空间可以访问内核空间,那么如果一个用户空间想与另外一个用户空间进行通信该怎么处理? + +那就是让操作系统内核添加支持,传统的linux通信机制,比如socket、管道等都是内核的一部分,因此通过内核支持来实现进程间通信自然是没有问题的,但是binder并不是linux系统内核的一部分,那它是怎么做到访问内核空间的呢?这就得益于 Linux 的**动态内核可加载模块**(Loadable Kernel Module,LKM)的机制;模块是具有独立功能的程序,它可以被单独编译,但是不能独立运行。它在运行时被链接到内核作为内核的一部分运行。这样,Android 系统就可以通过动态添加一个内核模块运行在内核空间,用户进程之间通过这个内核模块作为桥梁来实现通信。 + +> 在 Android 系统中,这个运行在内核空间,负责各个用户进程通过 Binder 实现通信的内核模块就叫 **Binder 驱动**(Binder Dirver)(尽管名叫‘驱动’,实际上和硬件设备没有任何关系,只是实现方式和设备驱动程序是一样的)。 + + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/binder_qudong.png) + + ## Android为什么要使用Binder @@ -44,61 +121,83 @@ Binder作为Android系统提供的一种IPC机制。首先一个问题就是为 **接下来正面回答这个问题,从5个角度来展开对Binder的分析:** -**(1)从性能的角度** :数据拷贝次数,Binder数据拷贝只需要一次,而管道、消息队列、Socket都需要2次,但共享内存方式一次内存拷贝都不需要;从性能角度看,Binder性能仅次于共享内存。 +- **从性能的角度** : -**(2)从稳定性的角度** -Binder是基于C/S架构的,简单解释下C/S架构,是指客户端(Client)和服务端(Server)组成的架构,Client端有什么需求,直接发送给Server端去完成,架构清晰明朗,Server端与Client端相对独立,稳定性较好;而共享内存实现方式复杂,没有客户与服务端之别, 需要充分考虑到访问临界资源的并发同步问题,否则可能会出现死锁等问题;从这稳定性角度看,Binder架构优越于共享内存。 + 对比与Linux的通信机制,socket是一个通用接口,导致其传输效率低、开销大。管道和消息队列因为采用存储转发的方式,所以至少需要拷贝2次数据,效率低。而共享内存虽然在传输时没有拷贝数据,但其控制机制复杂(比如跨进程通信时,需获取对方进程的pid,需要多种机制协同操作)。如果在APP级别,多拷贝一次或许没什么问题,但是如果上升到系统级别,系统内部通信频次是极高的,如果效率不够,用户体验会很差。 -仅仅从以上两点,各有优劣,还不足以支撑google去采用binder的IPC机制,那么更重要的原因是: + + + 具体原因如下,我们先来看看传统的 IPC 方式中,进程之间是如何实现通信的。 + + 通常的做法是消息发送方将要发送的数据存放在内存缓存区中,通过系统调用进入内核态。然后内核程序在内核空间分配内存,开辟一块内核缓存区,调用 copy*from*user() 函数将数据从用户空间的内存缓存区拷贝到内核空间的内核缓存区中。同样的,接收方进程在接收数据时在自己的用户空间开辟一块内存缓存区,然后内核程序调用 copy*to*user() 函数将数据从内核缓存区拷贝到接收进程的内存缓存区。这样数据发送方进程和数据接收方进程就完成了一次数据传输,我们称完成了一次进程间通信。 + + 这种传统的 IPC 通信方式有两个问题: -**(3)从安全的角度** -传统Linux IPC的接收方无法获得对方进程可靠的UID/PID,从而无法鉴别对方身份;而Android作为一个开放的开源体系,拥有非常多的开发平台,App来源甚广,因此手机的安全显得额外重要;对于普通用户,绝不希望从App商店下载偷窥隐私数据、后台造成手机耗电等等问题,传统Linux IPC无任何保护措施,完全由上层协议来确保。 + - 性能低下,一次数据传递需要经历:内存缓存区 --> 内核缓存区 --> 内存缓存区,需要 2 次数据拷贝; + - 接收数据的缓存区由数据接收进程提供,但是接收进程并不知道需要多大的空间来存放将要传递过来的数据,因此只能开辟尽可能大的内存空间或者先调用 API 接收消息头来获取消息体的大小,这两种做法不是浪费空间就是浪费时间。 -Android为每个安装好的应用程序分配了自己的UID,故进程的UID是鉴别进程身份的重要标志,前面提到C/S架构,**Android系统中对外只暴露Client端,Client端将任务发送给Server端,Server端会根据权限控制策略,判断UID/PID是否满足访问权限,目前权限控制很多时候是通过弹出权限询问对话框,让用户选择是否运行**。Android 6.0,也称为Android M,在6.0之前的系统是在App第一次安装时,会将整个App所涉及的所有权限一次询问,只要留意看会发现很多App根本用不上通信录和短信,但在这一次性权限权限时会包含进去,让用户拒绝不得,因为拒绝后App无法正常使用,而一旦授权后,应用便可以胡作非为。 + + + 那Binder是怎么操作的呢?这就不得不通道 Linux 下的另一个概念:**内存映射**。 + + Binder IPC 机制中涉及到的内存映射通过 mmap() 来实现,mmap() 是操作系统中一种内存映射的方法。内存映射简单的讲就是将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间;反之内核空间对这段区域的修改也能直接反应到用户空间。 + + 内存映射能减少数据拷贝次数,实现用户空间和内核空间的高效互动。两个空间各自的修改能直接反映在映射的内存区域,从而被对方空间及时感知。也正因为如此,内存映射能够提供对进程间通信的支持。 -针对这个问题,google在Android M做了调整,不再是安装时一并询问所有权限,而是在App运行过程中,需要哪个权限再弹框询问用户是否给相应的权限,对权限做了更细地控制,让用户有了更多的可控性,但同时也带来了另一个用户诟病的地方,那也就是权限询问的弹框的次数大幅度增多。对于Android M平台上,有些App开发者可能会写出让手机异常频繁弹框的App,企图直到用户授权为止,这对用户来说是不能忍的,用户最后吐槽的可不光是App,还有Android系统以及手机厂商,有些用户可能就跳果粉了,这还需要广大Android开发者以及手机厂商共同努力,共同打造安全与体验俱佳的Android手机。 + 一次完整的 Binder IPC 通信过程通常是这样: -Android中权限控制策略有SELinux等多方面手段。传统IPC只能由用户在数据包里填入UID/PID;另外,可靠的身份标记只有由IPC机制本身在内核中添加。其次传统IPC访问接入点是开放的,无法建立私有通道。从安全角度,Binder的安全性更高。 + 1. 首先 Binder 驱动在内核空间创建一个数据接收缓存区; -**(4)从语言层面的角度** -大家多知道Linux是基于C语言(面向过程的语言),而Android是基于Java语言(面向对象的语句),而对于Binder恰恰也符合面向对象的思想,将进程间通信转化为通过对某个Binder对象的引用调用该对象的方法,而其独特之处在于Binder对象是一个可以跨进程引用的对象,它的实体位于一个进程中,而它的引用却遍布于系统的各个进程之中。可以从一个进程传给其它进程,让大家都能访问同一Server,就像将一个对象或引用赋值给另一个引用一样。Binder模糊了进程边界,淡化了进程间通信过程,整个系统仿佛运行于同一个面向对象的程序之中。从语言层面,Binder更适合基于面向对象语言的Android系统,对于Linux系统可能会有点“水土不服”。 + 2. 接着在内核空间开辟一块内核缓存区,建立**内核缓存区**和**内核中数据接收缓存区**之间的映射关系,以及**内核中数据接收缓存区**和**接收进程用户空间地址**的映射关系; -**另外,Binder是为Android这类系统而生,而并非Linux社区没有想到Binder IPC机制的存在,对于Linux社区的广大开发人员,我还是表示深深佩服,让世界有了如此精湛而美妙的开源系统。**也并非Linux现有的IPC机制不够好,相反地,经过这么多优秀工程师的不断打磨,依然非常优秀,每种Linux的IPC机制都有存在的价值,同时在Android系统中也依然采用了大量Linux现有的IPC机制,根据每类IPC的原理特性,因时制宜,不同场景特性往往会采用其下最适宜的。比如在**Android OS中的Zygote进程的IPC采用的是Socket(套接字)机制**,Android中的**Kill Process采用的signal(信号)机制**等等。而**Binder更多则用在system_server进程与上层App层的IPC交互**。 + 3. 发送方进程通过系统调用 copy*from*user() 将数据 copy 到内核中的**内核缓存区**,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信。用户地址空间(服务端-数据接收端)和内核地址空间都映射到同一块物理地址空间。 -**(5) 从公司战略的角度** + Client(数据发送端)先从自己的用户进程空间把IPC数据通过copy_from_user()拷贝到内核空间。而Server端(数据接收端)与内核共享数据(mmap到同一块物理内存),不再需要拷贝数据,而是通过内存地址空间的偏移量,即可获悉内存地址,整个过程只发生一次内存拷贝。 + +- **从稳定性的角度** + Binder是基于C/S架构的,简单解释下C/S架构,是指客户端(Client)和服务端(Server)组成的架构,Client端有什么需求,直接发送给Server端去完成,架构清晰明朗,Server端与Client端相对独立,稳定性较好;而共享内存实现方式复杂,没有客户与服务端之别, 需要充分考虑到访问临界资源的并发同步问题,否则可能会出现死锁等问题;从这稳定性角度看,Binder架构优越于共享内存。 + +仅仅从以上两点,各有优劣,还不足以支撑google去采用binder的IPC机制,那么更重要的原因是: -总所周知,Linux内核是开源的系统,所开放源代码许可协议GPL保护,该协议具有“病毒式感染”的能力,怎么理解这句话呢?受GPL保护的Linux Kernel是运行在内核空间,对于上层的任何类库、服务、应用等运行在用户空间,一旦进行SysCall(系统调用),调用到底层Kernel,那么也必须遵循GPL协议。 +- **从安全的角度** + Linux的IPC机制在本身的实现中,并没有安全措施,得依赖上层协议来进行安全控制。而Android作为一个开放的开源体系,拥有非常多的开发平台,App来源甚广,因此手机的安全显得额外重要;对于普通用户,绝不希望从App商店下载偷窥隐私数据、后台造成手机耗电等等问题。Android为每个安装好的应用程序分配了自己的UID,故进程的UID是鉴别进程身份的重要标志,前面提到C/S架构,**Android系统中对外只暴露Client端,Client端将任务发送给Server端,Server端会根据权限控制策略,判断UID/PID是否满足访问权限,目前权限控制很多时候是通过弹出权限询问对话框,让用户选择是否运行**。Android 6.0,也称为Android M,在6.0之前的系统是在App第一次安装时,会将整个App所涉及的所有权限一次询问,只要留意看会发现很多App根本用不上通信录和短信,但在这一次性权限权限时会包含进去,让用户拒绝不得,因为拒绝后App无法正常使用,而一旦授权后,应用便可以胡作非为。 -而Android 之父 Andy Rubin对于GPL显然是不能接受的,为此,Google巧妙地将GPL协议控制在内核空间,将用户空间的协议采用Apache-2.0协议(允许基于Android的开发商不向社区反馈源码),同时在GPL协议与Apache-2.0之间的Lib库中采用BSD证授权方法,有效隔断了GPL的传染性,仍有较大争议,但至少目前缓解Android,让GPL止步于内核空间,这是Google在GPL Linux下开源与商业化共存的一个成功典范。 + 针对这个问题,google在Android M做了调整,不再是安装时一并询问所有权限,而是在App运行过程中,需要哪个权限再弹框询问用户是否给相应的权限,对权限做了更细地控制,让用户有了更多的可控性,但同时也带来了另一个用户诟病的地方,那也就是权限询问的弹框的次数大幅度增多。对于Android M平台上,有些App开发者可能会写出让手机异常频繁弹框的App,企图直到用户授权为止,这对用户来说是不能忍的,用户最后吐槽的可不光是App,还有Android系统以及手机厂商,有些用户可能就跳果粉了,这还需要广大Android开发者以及手机厂商共同努力,共同打造安全与体验俱佳的Android手机。 -**有了这些铺垫,我们再说说Binder的今世前缘** + Android中权限控制策略有SELinux等多方面手段。传统IPC只能由用户在数据包里填入UID/PID;另外,可靠的身份标记只有由IPC机制本身在内核中添加。其次传统IPC访问接入点是开放的,无法建立私有通道。从安全角度,Binder的安全性更高。 -Binder是基于开源的[OpenBinder](https://link.zhihu.com/?target=http%3A//www.angryredplanet.com/~hackbod/openbinder/docs/html/BinderIPCMechanism.html)实现的,OpenBinder是一个开源的系统IPC机制,最初是由 [Be Inc.](https://link.zhihu.com/?target=https%3A//en.wikipedia.org/wiki/Be_Inc.) 开发,接着由[Palm, Inc.](https://link.zhihu.com/?target=https%3A//en.wikipedia.org/wiki/Palm%2C_Inc.)公司负责开发,现在OpenBinder的作者在Google工作,既然作者在Google公司,在用户空间采用Binder 作为核心的IPC机制,再用Apache-2.0协议保护,自然而然是没什么问题,减少法律风险,以及对开发成本也大有裨益的,那么从公司战略角度,Binder也是不错的选择。 +- **从语言层面的角度** + 大家多知道Linux是基于C语言(面向过程的语言),而Android是基于Java语言(面向对象的语句),而对于Binder恰恰也符合面向对象的思想,将进程间通信转化为通过对某个Binder对象的引用调用该对象的方法,而其独特之处在于Binder对象是一个可以跨进程引用的对象,它的实体位于一个进程中,而它的引用却遍布于系统的各个进程之中。可以从一个进程传给其它进程,让大家都能访问同一Server,就像将一个对象或引用赋值给另一个引用一样。Binder模糊了进程边界,淡化了进程间通信过程,整个系统仿佛运行于同一个面向对象的程序之中。从语言层面,Binder更适合基于面向对象语言的Android系统,对于Linux系统可能会有点“水土不服”。 -另外,再说一点关于OpenBinder,在2015年OpenBinder以及合入到Linux Kernel主线 3.19版本,这也算是Google对Linux的一点回馈吧。 +- **从公司战略的角度** -**综合上述5点,可知Binder是Android系统上层进程间通信的不二选择。** + 总所周知,Linux内核是开源的系统,所开放源代码许可协议GPL保护,该协议具有“病毒式感染”的能力,怎么理解这句话呢?受GPL保护的Linux Kernel是运行在内核空间,对于上层的任何类库、服务、应用等运行在用户空间,一旦进行SysCall(系统调用),调用到底层Kernel,那么也必须遵循GPL协议。 + 而Android 之父 Andy Rubin对于GPL显然是不能接受的,为此,Google巧妙地将GPL协议控制在内核空间,将用户空间的协议采用Apache-2.0协议(允许基于Android的开发商不向社区反馈源码),同时在GPL协议与Apache-2.0之间的Lib库中采用BSD证授权方法,有效隔断了GPL的传染性,仍有较大争议,但至少目前缓解Android,让GPL止步于内核空间,这是Google在GPL Linux下开源与商业化共存的一个成功典范。 +综合上述5点,可知Binder是Android系统上层进程间通信的不二选择。 -**最后,简单讲讲Android Binder架构** -Binder在Android系统中江湖地位非常之高。在Zygote孵化出system_server进程后,在system_server进程中出初始化支持整个Android framework的各种各样的Service,而这些Service从大的方向来划分,分为Java层Framework和Native Framework层(C++)的Service,几乎都是基于BInder IPC机制: -1. Java framework:作为Server端继承(或间接继承)于Binder类,Client端继承(或间接继承)于BinderProxy类。例如 ActivityManagerService(用于控制Activity、Service、进程等) 这个服务作为Server端,间接继承Binder类,而相应的ActivityManager作为Client端,间接继承于BinderProxy类。 当然还有PackageManagerService、WindowManagerService等等很多系统服务都是采用C/S架构; -2. Native Framework层:这是C++层,作为Server端继承(或间接继承)于BBinder类,Client端继承(或间接继承)于BpBinder。例如MediaPlayService(用于多媒体相关)作为Server端,继承于BBinder类,而相应的MediaPlay作为Client端,间接继承于BpBinder类。 +### 疑问 +到这里又迷糊了,你上面说了这么多Binder这么好,那为啥Android里面还有地方用Socket?SystemServer和Zygote之间的通信为啥不用Binder? +SystemServer和Zygote之间通信不使用Socket的原因是为了要解决fork的问题。UNIX上C++程序设计守则3中规定:多线程程序里不准使用fork。 -**总之,一句话"无Binder不Android"。** +而Binder通讯是需要多线程操作的,代理对象对Binder的调用是在Binder线程,需要再通过Handler调用主线程来操作。比如AMS与应用进程通讯,AMS的本地代理IApplicationThread通过调用ScheduleLaunchActivity,调用到的应用进程ApplicationThread的ScheduleLaunchActivity是在Binder线程,需要再把参数封装为一个ActivityClientRecord,sendMessage发送给H类(主线程Handler,ActivityThread内部类)。主要原因是害怕父进程binder线程有锁,然后子进程的主线程一直在等待其子线程(从父进程拷贝过来的子进程)的资源,但是其实父进程的子进程并没有被拷贝过来,造成死锁。 +所以fork不允许存在多线程。而非常巧的是Binder通讯偏偏就是多线程,所以干脆父进程(Zygote)这个时候就不使用Binder线程了。 -前面人都说了Binder的优点,我来讲故事 + +所以也并非Linux现有的IPC机制不够好,相反地,经过这么多优秀工程师的不断打磨,依然非常优秀,每种Linux的IPC机制都有存在的价值,同时在Android系统中也依然采用了大量Linux现有的IPC机制,根据每类IPC的原理特性,因时制宜,不同场景特性往往会采用其下最适宜的。比如在Android OS中的Zygote进程的IPC采用的是Socket(套接字)机制,Android中的Kill Process采用的signal(信号)机制等等。而Binder更多则用在system_server进程与上层App层的IPC交互。 + +### 讲故事 1. 当年Andy Rubin有个公司Palm做掌上设备的就是当年那种PDA有个系统叫PalmOS后来palm被收购了以后 Andy Rubin创立了Android -2. Palm收购过一个公司叫Be里面有个移动系统叫BeOS,进程通信自己写了个实现叫Binder由一个叫Dianne Hackbod的人开发并维护后来Binder也被用到了PalmOS里 +2. Palm收购过一个公司叫Be里面有个移动系统叫BeOS,进程通信自己写了个实现叫OpenBinder由一个叫Dianne Hackbod的人开发并维护后来Binder也被用到了PalmOS里 3. Android创立了以后Andy从Palm带走了一大批人,其中就有Dianne。Dianne成为安卓系统总架构师。 @@ -114,73 +213,149 @@ Binder在Android系统中江湖地位非常之高。在Zygote孵化出system_ser + + ## Binder框架 -Binder是一种架构,这种架构提供了服务端接口、Binder驱动、客户端接口三个模块。 +在Android系统的Binder机制中,由一系统组件组成,分别是Client、Server、Service Manager和Binder驱动程序,其中Client、Server和Service Manager运行在用户空间,Binder驱动程序运行内核空间。其中Service Manager和Binder驱动由系统提供,而Client、Server由应用程序来实现。其中,核心组件便是Binder驱动程序了,Service Manager提供了辅助管理的功能,Client和Server正是在Binder驱动和Service Manager提供的基础设施上,进行Client-Server之间的通信。Client、Server 和 ServiceManager 均是通过系统调用 open、mmap 和 ioctl 来访问设备文件 /dev/binder,从而实现与Binder驱动的交互来间接的实现跨进程通信。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/binder_framework.png) -- 服务端 +- Server 一个Binder服务端实际上就是一个Binder类的对象,该对象一旦创建,内部就启动一个隐藏线程。该线程接下来会接收Binder驱动发送的消息,收到消息后,会执行到Binder对象中的onTransact()函数,并按照该函数的参数执行不同的服务代码。因此,要实现一个Binder服务就必须重载onTransact()方法。 可以想象,重载onTransact()函数的主要内容是把onTransact()函数的参数转换为服务函数的参数,而onTransact()函数的参数来源是客户端调用transact()函数时传入的,因此,如果transact()有固定格式的输入,那么onTransact()就会有固定格式的输出。 + ##### Binder线程池 + + 每个Server进程在启动时会创建一个binder线程池,并向其中注册一个Binder线程;之后Server进程也可以向binder线程池注册新的线程,或者Binder驱动在探测到没有空闲binder线程时会主动向Server进程注册新的的binder线程。对于一个Server进程有一个最大Binder线程数限制,默认为16个binder线程,例如Android的system_server进程就存在16个线程。对于所有Client端进程的binder请求都是交由Server端进程的binder线程来处理的。 + - Binder驱动 + 和路由器一样,Binder驱动虽然默默无闻,却是通信的核心。尽管名叫‘驱动’,实际上和硬件设备没有任何关系,只是实现方式和设备驱动程序是一样的:它工作于内核态,提供open(),mmap(),poll(),ioctl()等标准文件操作,以字符驱动设备中的misc设备注册在设备目录/dev下,用户通过/dev/binder访问该它。驱动负责进程之间Binder通信的建立,Binder在进程之间的传递,Binder引用计数管理,数据包在进程之间的传递和交互等一系列底层支持。驱动和应用程序之间定义了一套接口协议,主要功能由ioctl()接口实现,不提供read(),write()接口,因为ioctl()灵活方便,且能够一次调用实现先写后读以满足同步交互,而不必分别调用write()和read()。Binder驱动的代码位于linux目录的drivers/misc/binder.c中。 + + + 任何一个服务端Binder对象被创建时,同时会在Binder驱动中创建一个mRemote对象,该对象的类型也是Binder类。客户端要访问远程服务时,都是通过mRemote对象。 ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/binder_archi.png) -- 应用程序客户端 + 在Binder驱动层,每个接收端进程都有一个todo队列,用于保存发送端进程发送过来的binder请求,这类请求可以由接收端进程的任意一个空闲的binder线程处理;接收端进程存在一个或多个binder线程,在每个binder线程里都有一个todo队列,也是用于保存发送端进程发送过来的binder请求,这类请求只能由当前binder线程来处理。binder线程在空闲时进入可中断的休眠状态,当自己的todo队列或所属进程的todo队列有新的请求到来时便会唤醒,如果是由所需进程唤醒的,那么进程会让其中一个线程处理响应的请求,其他线程再次进入休眠状态。 - 客户端想要访问远程服务,必须获取远程服务在Binder对象中对应的mRemote引用,获得该mRemote对象后,就可以调用其transact()方法,调用该方法后,客户端线程进入Binder驱动,Binder驱动就会挂起当前线程,并向远程服务发送一个消息,消息中包含了客户端传进来的包裹。服务端拿到包裹后,会对包裹进行拆解,然后执行指定的服务函数,执行完毕后,再把执行结果放入客户端提供的reply包裹中。然后服务端向Binder驱动发送一个notify的消息,从而使得客户端线程从Binder驱动的代码区返回到客户端代码区。transact()的最后一个参数的含义是执行IPC调用的模式,分为两种:一种是双向,用常量0表示,其含义是服务端执行完指定的服务后返回一定的数据。另一种是单向,用常量1表示,其含义是不返回任何数据。最后,客户端就可以从reply中解析返回的数据了,同样,返回包裹中包含的数据也必须是有序的,而且这个顺序也必须是服务端和客户端事先约定好的。 + +- Client + + Server向ServiceManager注册了Binder实体及其名字后,Client就可以通过名字获得该Binder的引用了。Client也利用保留的0号引用向ServiceManager请求访问某个Binder:我申请获得名字叫张三的Binder的引用。ServiceManager收到这个连接请求,从请求数据包里获得Binder的名字,在查找表里找到该名字对应的条目,从条目中取出Binder的引用,将该引用作为回复发送给发起请求的Client。从面向对象的角度,这个Binder对象现在有了两个引用:一个位于ServiceManager中,一个位于发起请求的Client中。如果接下来有更多的Client请求该Binder,系统中就会有更多的引用指向该Binder,就象java里一个对象存在多个引用一样。而且类似的这些指向Binder的引用是强类型,从而确保只要有引用Binder实体就不会被释放掉。通过以上过程可以看出,ServiceManager象个火车票代售点,收集了所有火车的车票,可以通过它购买到乘坐各趟火车的票-得到某个Binder的引用。 + + ##### 匿名 Binder + + 并不是所有Binder都需要注册给ServiceManager广而告之的。Server端可以通过已经建立的Binder连接将创建的Binder实体传给Client,当然这条已经建立的Binder连接必须是通过实名Binder实现。由于这个Binder没有向ServiceManager注册名字,所以是个匿名Binder。Client将会收到这个匿名Binder的引用,通过这个引用向位于Server中的实体发送请求。匿名Binder为通信双方建立一条私密通道,只要Server没有把匿名Binder发给别的进程,别的进程就无法通过穷举或猜测等任何方式获得该Binder的引用,向该Binder发送请求。 + + + + 客户端想要访问远程服务,必须获取远程服务在Binder对象中对应的mRemote引用,获得该mRemote对象后,就可以调用其transact()方法,调用该方法后,客户端线程进入Binder驱动,Binder驱动就会挂起当前线程,并向远程服务发送一个消息,消息中包含了客户端传进来的包裹。服务端拿到包裹后,会对包裹进行拆解,然后执行指定的服务函数,执行完毕后,再把执行结果放入客户端提供的reply包裹中。然后服务端向Binder驱动发送一个notify的消息,从而使得客户端线程从Binder驱动的代码区返回到客户端代码区。transact()的最后一个参数的含义是执行IPC调用的模式,分为两种:一种是双向,用常量0表示,其含义是服务端执行完指定的服务后返回一定的数据。另一种是单向,用常量1表示,其含义是不返回任何数据。最后,客户端就可以从reply中解析返回的数据了,同样,返回包裹中包含的数据也必须是有序的,而且这个顺序也必须是服务端和客户端事先约定好的。 + 从这里可以看出,对应用程序开发员来说,客户端似乎是直接调用远程服务对应的Binder,而事实上则是通过Binder驱动进行了中转。即存在两个Binder对象,一个是服务端的Binder对象,另一个则是Binder驱动中的Binder对象,所不同的是Binder驱动中的对象不会再额外产生一个线程。 +- ServiceManager + + ServiceManager本身的工作很简单:注册服务、查询服务、列出所有服务,启动一个死循环来解析Binder驱动读写动作,进行事务处理。ServiceManager用于管理系统中的各种服务。架构图如下所示: + + ![ServiceManager](http://gityuan.com/images/binder/prepare/IPC-Binder.jpg) + + 可以看出无论是注册服务和获取服务的过程都需要ServiceManager(与DNS类似),需要注意的是此处的Service Manager是指Native层的ServiceManager(C++),并非指framework层的ServiceManager(Java)。ServiceManager是整个Binder通信机制的大管家,是Android进程间通信机制Binder的守护进程。Binder机制使用的是代理模式,在Server端的对象是实际对象,其他各个进程端所持有的都是Server端对象的Proxy代理对象,ServiceManager中会有一个类似map的结构,会存储Server端的信息,当Server端进程初始化时,会向ServiceManager中注册自己的信息。当client端想要访问时,也需要先向ServiceManager进行查询,当进行通信时,数据会流经内核空间中的binder驱动,此时驱动会对要传递的数据做转换。 + + 和DNS类似,ServiceManager的作用是将字符形式的Binder名字转化成Client中对该Binder的引用,使得Client能够通过Binder名字获得对Server中Binder实体的引用。注册了名字的Binder叫实名Binder,就象每个网站除了有IP地址外还有自己的网址。Server创建了Binder实体,为其取一个字符形式,可读易记的名字,将这个Binder连同名字以数据包的形式通过Binder驱动发送给ServiceManager,通知ServiceManager注册一个名叫张三的Binder,它位于某个Server中。驱动为这个穿过进程边界的Binder创建位于内核中的实体节点以及ServiceManager对实体的引用,将名字及新建的引用打包传递给ServiceManager。ServiceManager收数据包后,从中取出名字和引用填入一张查找表中。 + + 细心的读者可能会发现其中的蹊跷:ServiceManager是一个进程,Server是另一个进程,Server向ServiceManager注册Binder必然会涉及进程间通信。当前实现的是进程间通信却又要用到进程间通信,这就好象蛋可以孵出鸡前提却是要找只鸡来孵蛋。Binder的实现比较巧妙:预先创造一只鸡来孵蛋:ServiceManager和其它进程同样采用Binder通信,ServiceManager是Server端,有自己的Binder对象(实体),其它进程都是Client,需要通过这个Binder的引用来实现Binder的注册,查询和获取。ServiceManager提供的Binder比较特殊,它没有名字也不需要注册,当一个进程使用BINDER_SET_CONTEXT_MGR命令将自己注册成ServiceManager时Binder驱动会自动为它创建Binder实体(这就是那只预先造好的鸡)。其次这个Binder的引用在所有Client中都固定为0(handle=0)而无须通过其它手段获得。也就是说,一个Server若要向ServiceManager注册自己Binder就必须通过0这个引用号和ServiceManager的Binder通信。类比网络通信,0号引用就好比域名服务器的地址,你必须预先手工或动态配置好。要注意这里说的Client是相对ServiceManager而言的,一个应用程序可能是个提供服务的Server,但对ServiceManager来说它仍然是个Client。 + + 图中的Client,Server,Service Manager之间交互都是虚线表示,是由于它们彼此之间不是直接交互的,而是都通过与[Binder驱动](http://gityuan.com/2015/11/01/binder-driver/)进行交互的。 + + ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/binder_client_server.png) + + + + + -## Binder IPC原理 +## Binder 通信过程 +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/binder_communication.jpg) -每个Android的进程,只能运行在自己进程所拥有的虚拟地址空间。对应一个4GB的虚拟地址空间,其中3GB是用户空间,1GB是内核空间,当然内核空间的大小是可以通过参数配置调整的。对于用户空间,不同进程之间彼此是不能共享的,而内核空间却是可共享的。Client进程向Server进程通信,恰恰是利用进程间可共享的内核内存空间来完成底层通信工作的,Client端与Server端进程往往采用ioctl(设备驱动程序中设备控制接口函数,进程与内核通信的一种方法)等方法跟内核空间的驱动进行交互。 +- 首先,一个进程使用 BINDER*SET*CONTEXT_MGR 命令通过 Binder 驱动将自己注册成为 ServiceManager; +- Server通过驱动向ServiceManager中进行服务注册,表明可以对外提供服务。ServiceManager有一个全局的service列表svcinfo,用来缓存所有服务的handler和name。驱动为这个Binder创建位于内核中的实体节点以及 ServiceManager 对实体的引用,将名字以及新建的引用打包传给 ServiceManager,ServiceManger 将其填入查找表。 -Binder通信采用C/S架构,从组件视角来说,包含Client、Server、ServiceManager以及binder驱动,其中ServiceManager用于管理系统中的各种服务。架构图如下所示: +- 客户端与服务端通信,需要拿到服务端的对象,由于进程隔离,客户端拿到的其实是服务端的代理,也可以理解为引用。客户端通过Client 通过名字,在 Binder 驱动的帮助下从ServiceManager的svcinfo中查找服务,ServiceManager返回服务的代理。 -![ServiceManager](http://gityuan.com/images/binder/prepare/IPC-Binder.jpg) +- Server进程启动之后,会进入中断等待状态,等待Client的请求。 -可以看出无论是注册服务和获取服务的过程都需要ServiceManager,需要注意的是此处的Service Manager是指Native层的ServiceManager(C++),并非指framework层的ServiceManager(Java)。ServiceManager是整个Binder通信机制的大管家,是Android进程间通信机制Binder的守护进程。 +- 当Client需要和Server通信时,会通过BinderProxy将我们的请求参数发送给 内核,通过共享内存的方式使用内核方法 copy_from_user() 将我们的参数先拷贝到内核空间,这时我们的客户端进入等待状态。 -图中Client/Server/ServiceManage之间的相互通信都是基于Binder机制。既然基于Binder机制通信,那么同样也是C/S架构,则图中的3大步骤都有相应的Client端与Server端。 +- Binder驱动收到请求之后,会唤醒Server进程,Binder驱动向服务端的 todo 队列里面插入一条事务,执行完之后把执行结果通过 copy_to_user() 将内核的结果拷贝到用户空间。 +- Server进程解析出请求内容,并将回复内容发送给Binder驱动。 +- 接着,Binder驱动收到回复之后,唤醒Client进程。Binder驱动还会反馈信息给Client,告诉Client:它发送给Binder驱动的请求,Binder驱动已经完成。 +- 接着,Binder驱动还会反馈信息给Server,告诉Server:它发送给Binder驱动的回复,Binder驱动已经收到。 +- Server将回复发送成功之后,再次进入等待状态,等待Client的请求。 +- 最后,Binder驱动将回复转发给Client。 -1. **[注册服务(addService)](http://gityuan.com/2015/11/14/binder-add-service/)**:Server进程要先注册Service到ServiceManager。该过程:Server是客户端,ServiceManager是服务端。 -2. **[获取服务(getService)](http://gityuan.com/2015/11/15/binder-get-service/)**:Client进程使用某个Service前,须先向ServiceManager中获取相应的Service。该过程:Client是客户端,ServiceManager是服务端。 -3. **使用服务**:Client根据得到的Service信息建立与Service所在的Server进程通信的通路,然后就可以直接与Service交互。该过程:client是客户端,server是服务端。 -图中的Client,Server,Service Manager之间交互都是虚线表示,是由于它们彼此之间不是直接交互的,而是都通过与[Binder驱动](http://gityuan.com/2015/11/01/binder-driver/)进行交互的,从而实现IPC通信方式。其中Binder驱动位于内核空间,Client,Server,Service Manager位于用户空间。Binder驱动和Service Manager可以看做是Android平台的基础架构,而Client和Server是Android的应用层,开发人员只需自定义实现client、Server端,借助Android的基本平台架构便可以直接进行IPC通信。 +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/binder_all_process.jpg) +## Binder的内存管理 +ServiceManager启动后,会通过系统调用mmap向内核空间申请128K的内存,用户进程会通过mmap向内核申请(1M-8K)的内存空间。 +这里用户空间mmap (1M-8K)的空间,为什么要减去8K,而不是直接用1M? -### 2.3 C/S模式 +Android的git commit记录: -BpBinder(客户端)和BBinder(服务端)都是Android中Binder通信相关的代表,它们都从IBinder类中派生而来,关系图如下: +> Modify the binder to request 1M - 2 pages instead of 1M. The backing store in the kernel requires a guard page, so 1M allocations fragment memory very badly. Subtracting a couple of pages so that they fit in a power of two allows the kernel to make more efficient use of its virtual address space. -![Binder关系图](http://gityuan.com/images/binder/prepare/Ibinder_classes.jpg) +大致的意思是:kernel的“backing store”需要一个保护页,这使得1M用来分配碎片内存时变得很差,所以这里减去两页来提高效率,因为减去一页就变成了奇数。 -- client端:BpBinder.transact()来发送事务请求; -- server端:BBinder.onTransact()会接收到相应事务。 +​ +**系统定义:**BINDER_VM_SIZE ((1 * 1024 * 1024) - sysconf(_SC_PAGE_SIZE) * 2) = (1M- sysconf(_SC_PAGE_SIZE) * 2) +**这里的8K,其实就是两个PAGE的SIZE, 物理内存的划分是按PAGE(页)来划分的,一般情况下,一个Page的大小为4K。** + +**内核会增加一个guard page,再加上内核本身的guard page,正好是两个page的大小,减去后,就是用户空间可用的大小。** + +在内存分配这块,还要分为32位和64位,32位的系统很好区分,虚拟内存为4G,用户空间从低地址开始占用3G,内核空间占用剩余的1G。 + +ARM32内存占用分配: + +![img](https://img-blog.csdnimg.cn/20200329164623413.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3lpcmFuZmVuZw==,size_16,color_FFFFFF,t_70) + +但随着现在的硬件发展越来越迅速,应用程序的运算也越来越复杂,占用空间越来越大,原有的4G虚拟内存已经不能满足用户的需求,因此,现在的Android基本都是用64位的内存机制。 + +理论上讲,64位的地址总线可以支持高达16EB(2^64)的内存。AMD64架构支持52位(4PB)的地址总线和48位(256TB)的虚拟地址空间。在linux arm64中,如果页的大小为4KB,使用3级页表转换或者4级页表转换,用户空间和内核空间都支持有39bit(512GB)或者48bit(256TB)大小的虚拟地址空间。 + +2^64 次方太大了,Linux 内核只采用了 64 bits 的一部分(开启 CONFIG_ARM64_64K_PAGES 时使用 42 bits,页大小是 4K 时使用 39 bits),该文假设使用的页大小是 4K(VA_BITS = 39) + +ARM64 有足够的虚拟地址,用户空间和内核空间可以有各自的 2^39 = 512GB 的虚拟地址。 + +ARM64内存占用分配: + +![img](https://img-blog.csdnimg.cn/20200329164637770.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3lpcmFuZmVuZw==,size_16,color_FFFFFF,t_70) + +用户地址空间(服务端-数据接收端)和内核地址空间都映射到同一块物理地址空间。 参考: https://www.zhihu.com/question/39440766/answer/93550572 +[https://zhuanlan.zhihu.com/p/35519585](https://zhuanlan.zhihu.com/p/35519585) + --- diff --git "a/OperatingSystem/AndroidKernal/3.Android Framework\346\241\206\346\236\266.md" "b/OperatingSystem/AndroidKernal/3.Android Framework\346\241\206\346\236\266.md" index f550d1ee..0e052371 100644 --- "a/OperatingSystem/AndroidKernal/3.Android Framework\346\241\206\346\236\266.md" +++ "b/OperatingSystem/AndroidKernal/3.Android Framework\346\241\206\346\236\266.md" @@ -29,13 +29,28 @@ AmS的作用是管理所有应用程序中的Activity。 客户端主要包括以下重要类: -- ActivityThread类:该类为应用程序的主线程类,所有的APK程序都有且仅有一个ActivityThread类,程序的入口为该类中的static main()函数。 +- ActivityThread类:该类为应用程序的主线程类,所有的APK程序都有且仅有一个ActivityThread类,程序的入口为该类中的static main()函数。ActivityThread是Android Framework中一个非常重要的类,它代表一个应用进程的主线程,其职责就是调度及执行在该线程中运行的四大组件。 + + 注意到此处的ActivityThread创建于SystemServer进程中。 + + 由于SystemServer中也运行着一些系统APK,例如framework-res.apk、SettingsProvider.apk等,因此也可以认为SystemServer是一个特殊的应用进程。 + + AMS负责管理和调度进程,因此AMS需要通过Binder机制和应用进程通信。 + + 为此,Android提供了一个IApplicationThread接口,该接口定义了AMS和应用进程之间的交互函数。 + - Activity类:该类为APK程序中的一个最小运行单元,一个APK程序中可以包含多个Activity对象,ActivityThread类会根据用户操作选择运行哪个Activity对象。 + - PhoneWindow类:该类继承于Window类,同时,PhoneWindow类内部包含了一个DecorView对象。简而言之,PhoneWindow是把一个FrameLayout进行了一定的包装,并提供了一组通用的窗口操作接口。 + - Window类:该类提供了一组通用的窗口(Window)操作API,这里的窗口仅仅是程序层面上的,WmS所管理的窗口并不是Window类,而是一个View或者ViewGroup类,一般就是指DecorView类,即一个DecorView就是WmS所管理的一个窗口。Window是一个abstract类型。 + - DecorView类:该类是一个FrameLayout的子类,并且是PhoneWindow中的一个内部类。Decor的英文是Decoration,即”装饰“的意思,DecorView就是对普通的FrameLayout进行了一定的装饰,比如添加一个通用的Titlebar,并响应特定的按键消息等。 + - ViewRoot类:WmS管理客户端窗口时,需要通知客户端进行某种操作,这些都是通过异步消息完成的,实现的方式就是使用Handler,ViewRoot类就是继承于Handler,其作用主要是接收WmS的通知。 + - W类:该类继承于Binder,并且是ViewRoot的一个内部类。 + - WindowManager类:客户端要申请创建一个窗口,而具体创建窗口的任务是由WmS完成的,WindowManager类就像是一个部门经理,谁有什么需求就告诉它,由它和WmS进行交互,客户端不能直接和WmS进行交互。 diff --git "a/VideoDevelopment/\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/1.\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" similarity index 97% rename from "VideoDevelopment/\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" rename to "VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/1.\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" index a6e48d63..7af53a77 100644 --- "a/VideoDevelopment/\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" +++ "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/1.\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" @@ -36,7 +36,7 @@ 比特率恒定,图像内容复杂的片段质量不稳定,图像内容简单的片段质量较好。 -- ***帧率***:帧/秒(`frames per second`)的缩写帧率即每秒显示帧数,帧率表示图形处理器处理场时每秒钟能够更新的次数。高的帧率可以得到更流畅、更逼真的动画。一般来说`30fps`就是可以接受的,但是将性能提升至`60fps`则可以明显提升交互感和逼真感,但是一般来说超过`75fps`一般就不容易察觉到有明显的流畅度提升了。如果帧率超过屏幕刷新率只会浪费图形处理的能力,因为监视器不能以这么快的速度更新,这样超过新率的帧率就浪费掉了。 +- ***帧率(Frame Rate)***:帧/秒(`frames per second`)的缩写帧率即每秒显示帧数,帧率表示图形处理器处理场时每秒钟能够更新的次数。高的帧率可以得到更流畅、更逼真的动画。一般来说`30fps`就是可以接受的,但是将性能提升至`60fps`则可以明显提升交互感和逼真感,但是一般来说超过`75fps`一般就不容易察觉到有明显的流畅度提升了。如果帧率超过屏幕刷新率只会浪费图形处理的能力,因为监视器不能以这么快的速度更新,这样超过新率的帧率就浪费掉了。 ![image](https://github.com/CharonChui/Pictures/blob/master/fps.gif?raw=true) - ***关键帧***:相当于二维动画中的原画,指角色或者物体运动或变化中的关键动作所处的那一帧,它包含了图像的所有信息,后来帧仅包含了改变了的信息。如果你没有足够的关键帧,你的影片品质可能比较差,因为所有的帧从别的帧处产生。对于一般的用途,一个比较好的原则是每5秒设一个关键键。但如果时那种实时传输的流文件,那么要考虑传输网络的可靠度,所以要1到2秒增加一个关键帧。 From 1750ce75d3e7d0f0e09b3123f1bd239cc0376fa6 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 31 Dec 2020 14:13:11 +0800 Subject: [PATCH 042/213] udpate --- .../3.\345\206\205\345\255\230\347\256\241\347\220\206.md" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" "b/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" index fa5e3048..33824b74 100644 --- "a/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" +++ "b/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" @@ -243,7 +243,7 @@ Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自 Android包含了标准Linux内核中内存管理设施的许多扩展,具体如下: -- ASHMem:这个功能提供匿名共享内存,它将内存抽象为文件描述符。文件描述符可以传递给另一个进程以共享内存。 +- ASHMem(Anonymous Shared Memory):这个功能提供匿名共享内存,它将内存抽象为文件描述符。文件描述符可以传递给另一个进程以共享内存。 - Pmen:这个功能分配虚拟内存,使的它在物理上是连续的,因此对于那些不支持虚拟内存的设备非常实用。 From 3d5dc96ccef71bfe1313139381c88778f72c3e83 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 11 Jan 2021 15:13:08 +0800 Subject: [PATCH 043/213] update --- ...73\347\273\237\347\256\200\344\273\213.md" | 158 ++++++++++-- ...13\344\270\216\347\272\277\347\250\213.md" | 237 ++++++++++++++++-- ...05\345\255\230\347\256\241\347\220\206.md" | 63 ++++- OperatingSystem/5.I:O.md | 71 ++++++ ...07\344\273\266\347\256\241\347\220\206.md" | 74 ++++++ ...13\351\227\264\351\200\232\344\277\241.md" | 54 +++- ...72\347\241\200\347\237\245\350\257\206.md" | 146 +++++++---- ...255\346\224\276\345\231\250MediaPlayer.md" | 30 +++ 8 files changed, 737 insertions(+), 96 deletions(-) create mode 100644 "VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/2.\347\263\273\347\273\237\346\222\255\346\224\276\345\231\250MediaPlayer.md" diff --git "a/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" index 82ba0f2a..0b4d80e4 100644 --- "a/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" +++ "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" @@ -10,6 +10,15 @@ +操作系统也是一种软件,但是操作系统是一种非常复杂的软件。操作系统提供了几种抽象模型 + +- 文件:对 I/O 设备的抽象 +- 虚拟内存:对程序存储器的抽象 +- 进程:对一个正在运行程序的抽象 +- 虚拟机:对整个操作系统的抽象 + + + ### 以现代标准而言,一个标准PC的操作系统应该提供以下功能 @@ -76,7 +85,7 @@ 计算机由处理器、存储器和输入/输出部件组成,每类部件都有一个或多个模块。这些部件以某种方式互连,以实现计算机执行程序的主要功能。因此,计算机有4个主要的结构化部件: -- 处理器(Processor):控制计算机的操作,执行数据处理功能。只有一个处理器时,它通常指中央处理器(CPU) +- 处理器(Processor):控制计算机的操作,执行数据处理功能。只有一个处理器时,它通常指中央处理器(CPU). - 内存(Main memory):存储数据和程序。此类存储器通常是易失性的,即当计算机关机时,存储器的内容会丢失。相对于此的是磁盘存储器,当计算机关机时,它的内容不会丢失。内存通常也称为实存储器(real memory)或主存储器(primary memory)。 - 输入/输出模块(I/O modules):在计算机和外部环境之间移动数据。外部环境由各种外部设备组成,包括辅助存储器设备(如硬盘)、通信设备和终端。 - 系统总线(System bus):在处理器、内存和输入/输出模块间提供通信的设施。 @@ -148,9 +157,32 @@ Linux内核被装载后,就开始进行内核初始化的过程。 ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_system_start.png) +- Boot ROM: 当手机处于关机状态时,长按Power键开机,引导芯片开始从固化在`ROM`里的预设代码开始执行,然后加载引导程序到`RAM`; +- Boot Loader:这是启动Android系统之前的引导程序,主要是检查RAM,初始化硬件参数等功能。 +Android平台的基础是Linux内核,比如ART虚拟机最终调用底层Linux内核来执行功能。Linux内核的安全机制为Android提供相应的保障,也允许设备制造商为内核开发硬件驱动程序。 +- 启动Kernel的swapper进程(pid=0):该进程又称为idle进程, 系统初始化过程Kernel由无到有开创的第一个进程, 用于初始化进程管理、内存管理,加载Display,Camera Driver,Binder Driver等相关工作; +- 启动kthreadd进程(pid=2):是Linux系统的内核进程,会创建内核工作线程kworkder,软中断线程ksoftirqd,thermal等内核守护进程。`kthreadd进程是所有内核进程的鼻祖`。 +这里的Native系统库主要包括init孵化来的用户空间的守护进程、HAL层以及开机动画等。启动init进程(pid=1),是Linux系统的用户进程,`init进程是所有用户进程的鼻祖`。 + +- init进程会孵化出ueventd、logd、healthd、installd、adbd、lmkd等用户守护进程; +- init进程还启动`servicemanager`(binder服务管家)、`bootanim`(开机动画)等重要服务 +- init进程孵化出Zygote进程,Zygote进程是Android系统的第一个Java进程(即虚拟机进程),`Zygote是所有Java进程的父进程`,Zygote进程本身是由init进程孵化而来的。 + +- Zygote进程,是由init进程通过解析init.rc文件后fork生成的,Zygote进程主要包含: + - 加载ZygoteInit类,注册Zygote Socket服务端套接字 + - 加载虚拟机 + - 提前加载类preloadClasses + - 提前加载资源preloadResouces +- System Server进程,是由Zygote进程fork而来,`System Server是Zygote孵化的第一个进程`,System Server负责启动和管理整个Java framework,包含ActivityManager,WindowManager,PackageManager,PowerManager等服务。 +- Media Server进程,是由init进程fork而来,负责启动和管理整个C++ framework,包含AudioFlinger,Camera Service等服务。 +- Zygote进程孵化出的第一个App进程是Launcher,这是用户看到的桌面App; +- Zygote进程还会创建Browser,Phone,Email等App进程,每个App至少运行在一个进程上。 +- 所有的App进程都是由Zygote进程fork生成的。 + + ## CPU(Central Processing Unit) @@ -158,25 +190,35 @@ CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从 -**程序是把寄存器作为对象来描述的** +CPU 主要由两部分构成:`控制单元` 和 `算术逻辑单元(ALU)` -使用高级语言编写的程序会在编译后转化成机器语言,然后通过CPU内部的寄存器来处理。不同类型的CPU,其内部寄存器的数量,种类以及寄存器存储的数值范围都是不同的。根据功能的不同,我们可以将寄存器大致划分为八类: +- 控制单元:从内存中提取指令并解码执行 +- 算数逻辑单元(ALU):处理算数和逻辑运算 -- ***累加寄存器简称累加器(Accumulator, AC)***:是一个通用寄存器。存储临时的执行运算的数据和运算后的数据。 +CPU 是计算机的心脏和大脑,它和内存都是由许多晶体管组成的电子部件。它接收数据输入,执行指令并处理信息。它与输入/输出(I / O)设备进行通信,这些设备向 CPU 发送数据和从 CPU 接收数据。 -- ***标志寄存器***:存储运算处理后的CPU的状态。 +CPU 的内部由**寄存器、控制器、运算器和时钟**四部分组成,各部分之间通过电信号连通。 -- ***程序计数器(Program Counter, PC)***:存储下一条指令所在内存的地址。 +- `寄存器`是中央处理器内的组成部分。它们可以用来暂存指令、数据和地址。可以将其看作是内存的一种。根据种类的不同,一个 CPU 内部会有 20 - 100个寄存器。 +- `控制器`负责把内存上的指令、数据读入寄存器,并根据指令的结果控制计算机 +- `运算器`负责运算从内存中读入寄存器的数据 +- `时钟` 负责发出 CPU 开始计时的时钟信号 -- ***基址寄存器***:存储数据内存的起始地址。 -- ***变址寄存器***:存储基址寄存器的相对地址。 -- ***通用寄存器***:存储任意数据。 +**程序是把寄存器作为对象来描述的** -- ***指令寄存器(Instruction Register, IR)***:存储指令,CPU取到的指令存放在处理器的一个寄存器中,这个寄存器就是指令寄存器。CPU内部使用,程序员无法通过程序对该寄存器进行读写操作。 +使用高级语言编写的程序会在编译后转化成机器语言,然后通过CPU内部的寄存器来处理。不同类型的CPU,其内部寄存器的数量,种类以及寄存器存储的数值范围都是不同的。根据功能的不同,我们可以将寄存器大致划分为八类: -- ***栈寄存器***:存储栈区域的起始地址。 +- ***累加寄存器简称累加器(Accumulator, AC)***:是一个通用寄存器。存储临时的执行运算的数据和运算后的数据。 +- ***标志寄存器***:存储运算处理后的CPU的状态。 +- ***程序计数器(Program Counter, PC)***:记录将要取出的指令的地址。存储下一条指令所在内存的地址。 +- ***基址寄存器***:存储数据内存的起始地址。 +- ***变址寄存器***:存储基址寄存器的相对地址。 +- ***通用寄存器***:存储任意数据。 +- ***指令寄存器(Instruction Register, IR)***:记录最近取出的指令。存储指令,CPU取到的指令存放在处理器的一个寄存器中,这个寄存器就是指令寄存器。CPU内部使用,程序员无法通过程序对该寄存器进行读写操作。 +- ***堆栈寄存器(stack pointer)***:目的是跟踪调用堆栈,存储栈区域的起始地址。指向内存中当前栈的顶端。堆栈指针会包含输入过程中的有关参数、局部变量以及没有保存在寄存器中的临时变量。 +- **程序状态字寄存器(PSW(Program Status Word))**:这个寄存器是由操作系统维护的8个字节(64位) long 类型的数据集合。它会跟踪当前系统的状态。除非发生系统结束,否则我们可以忽略 PSW 。用户程序通常可以读取整个PSW,但通常只能写入其某些字段。PSW 在系统调用和 I / O 中起着重要作用。 其中,程序计数器,累加寄存器,标志寄存器,指令寄存器和栈寄存器都只有一个,其他的寄存器一般有多个。 @@ -230,11 +272,25 @@ CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从 -### 中断 +#### 操作系统的两种CPU状态 + +- 内核态(Kernel Mode):运行操作系统程序 +- 用户态(User Mode):运行用户程序 + +操作系统只需要这两种状态,同时这两种状态由对应的两种指令: +- 特权(privilege)指令:只能由操作系统使用、用户程序不能使用的指令 +- 非特权指令:用户程序可以使用的指令 +用户态 -> 内核态的唯一途径是通过中断/异常/陷入机制。 +内核态 -> 用户态是通过设置程序状态字PSW。 +一旦 CPU 决定去实施中断后,程序计数器和 PSW 就会被压入到当前堆栈中并且 CPU 会切换到内核态。设备编号可以作为内存的一个引用,用来寻找该设备中断处理程序的地址。这部分内存称作`中断向量(interrupt vector)`。一旦中断处理程序(中断设备的设备驱动程序的一部分)开始后,它会移除栈中的程序计数器和 PSW 寄存器,并把它们进行保存,然后查询设备的状态。在中断处理程序全部完成后,它会返回到先前用户程序尚未执行的第一条指令。 + +### 中断/异常 + +可以说操作系统是由“中断驱动”或者“时间驱动”的。中断/异常是CPU对系统发生的某个事件作出的一种反应。 所有计算机都提供了允许其他模块(I/O、存储器)中断处理器正常处理过程的机制。中断最初是用于提高处理器效率的一种手段。例如,多数I/O设备都要远慢于处理器,处理器必须暂停并保持空闲,直到打印机完成工作。暂停的时间长度可能相当于成百上千个不涉及存储器的指令周期,显然,这对于处理器的使用来说是非常浪费的。这种只有一个单独程序的情况,称为单道程序设计。在单道程序设计中处理器话费一定的运行时间进行计算,直到遇到一个I/O指令,这时它必须等到该I/O指令结束后才能继续执行。这种问题是可以避免的,就是存储器可以保存多个程序,在一个程序等待时通过切换去执行其他的程序,这种处理称为多道程序设计或多任务处理。它是现代操作系统的主要方案。多道程序设计的目的是为了让处理器和I/O设备(包括存储设备)同时保持忙状态,以实现最大的效率。 @@ -250,12 +306,38 @@ CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从 -### 中断处理 +### 举个栗子 + +- 中断 + + 有一天我正在看书的时候,来电话了,这时候我需要执行中断,我会把当前看到的书的进度用书签标记,然后去接电话,等电话接完后,再返回从书签的位置继续读书。 + +- 异常 + + 同样我在读书,但是由于新买的书纸张太硬,一不小心把手划破流血了,那这个时候就是异常,我同样需要把当前看到的书的进度用书签标记,然后去用创可贴来处理伤口,等处理完后再返回书签的位置继续读书。 +### 中断/异常机制工作原理 + +硬件和软件相互配合而使计算机系统得以充分发挥能力。 + +- 硬件的作用 -- 中断/异常响应 + + 捕获中断源发出的中断/异常请求,以一定方式响应,将处理器控制权交给特定的处理程序。发现中断、接受中断都是由硬件来完成。 + +- 软件的作用 -- 中断/异常处理程序 + + 识别中断/异常类型并完成相应的处理。 + + + + + +### 中断处理 + 当I/O设备完成一次I/O操作时,发生以下硬件事件: 1. 设备给处理器发送一个中断信号。 @@ -270,6 +352,21 @@ CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从 +与每一 I/O 类相关联的是一个称作 `中断向量(interrupt vector)` 的位置(靠近内存底部的固定区域)。它包含中断服务程序的入口地址。假设当一个磁盘中断发生时,用户进程 3 正在运行,则中断硬件将程序计数器、程序状态字、有时还有一个或多个寄存器压入堆栈,计算机随即跳转到中断向量所指示的地址。这就是硬件所做的事情。然后软件就随即接管一切剩余的工作。 + +当中断结束后,操作系统会调用一个 C 程序来处理中断剩下的工作。在完成剩下的工作后,会使某些进程就绪,接着调用调度程序,决定随后运行哪个进程。然后将控制权转移给一段汇编语言代码,为当前的进程装入寄存器值以及内存映射并启动该进程运行,下面显示了中断处理和调度的过程。 + +1. 硬件压入堆栈程序计数器等 +2. 硬件从中断向量装入新的程序计数器 +3. 汇编语言过程保存寄存器的值 +4. 汇编语言过程设置新的堆栈 +5. C 中断服务器运行(典型的读和缓存写入) +6. 调度器决定下面哪个程序先运行 +7. C 过程返回至汇编代码 +8. 汇编语言过程开始运行新的当前进程 + +一个进程在执行过程中可能被中断数千次,但关键每次中断后,被中断的进程都返回到与中断发生前完全相同的状态。 + ## 存储器 @@ -377,10 +474,6 @@ CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从 -![](https://raw.githubusercontent.com/CharonChui/Pictures/master/![](https://raw.githubusercontent.com/CharonChui/Pictures/master/rtsp_rtp_rtcp.jpeg)) - - - - 应用和框架:应用开发者最关心这一层及访问低层服务的API。 - Binder IPC:Binder进程间通信机制允许应用框架打破进程的界限来访问Android系统服务代码,从而允许系统的高层框架API与Android的系统服务进行交互。 - Android系统服务:框架中大部分能够调用系统服务的接口都向开发者开放,以便开发者能够使用底层的硬件和内核功能。Android系统服务分为两部分:媒体服务处理播放和录制媒体文件,系统服务处理应用所需要的系统功能。 @@ -403,17 +496,44 @@ Android在Linux内核中增加了两个提升电源管理能力的新功能: +## 一个程序的执行过程 +```c +#include +int main(int argc, char *argv[]) { + puts("hello world"); + return 0; +} +``` ----- -- [下一篇:2.进程与线程](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/2.%E8%BF%9B%E7%A8%8B%E4%B8%8E%E7%BA%BF%E7%A8%8B.md) + +1. 用户通过命令或者图标点击等通知操作系统执行hello world程序。 +2. 操作系统会去找到hello world程序的相关信息,检查其类型是否是可执行文件,并通过程序的首部信息,确定代码和数据在可执行文件中的位置并计算出对应的磁盘块地址。 +3. 操作系统创建一个新的进程,并将hello world程序的执行文件映射到该进程结构,表示由该进程执行该hello world程序。 +4. 操作系统为hello world程序设置cpu上下文环境并跳到程序开始处。 +5. 执行hello world程序的第一条指令,这时候会发生缺页异常(内存中没有该程序) +6. 操作系统开始分配一页物理内存,并将前面计算出的磁盘块地址将代码从磁盘读入内存,然后继续执行hello world程序。 +7. hello world程序执行puts函数(系统调用),想要在显示器上写入字符串。 +8. 操作系统找到要将字符串送往的显示设备,通常设备是由一个进程控制的,所以,操作系统将要写的字符串送给该进程。 +9. 控制设备的进程告诉设备的窗口系统它要显示字符串。窗口系统确定这是一个合法的操作,然后将字符串转换成像素,将像素写入设备的存储影响区。 +10. 视频硬件将像素转换成显示器可接收的一组控制/数据信号。 +11. 显示器解释信号,激发液晶屏。 +12. 这样我们就能在屏幕上看到了"hello world"。 + +---- + +- [下一篇:2.进程与线程](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/2.%E8%BF%9B%E7%A8%8B%E4%B8%8E%E7%BA%BF%E7%A8%8B.md) + + + + --- diff --git "a/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" index 8dd5f952..c96b8f66 100644 --- "a/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" +++ "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" @@ -1,14 +1,18 @@ # 2.进程与线程 +### 为什么要引入进程的概念? +我们知道I/O操作是耗时的,CPU在执行的一个程序的时候这个程序可能会有一些耗时的操作,而CPU的执行是非常快的,那如果这种情况下,CPU一直等待这个程序执行完耗时的操作再继续执行,就会导致CPU的使用率大大降低,为了提高CPU的使用率,就引入了多道程序设计,也就是说有多个程序执行,当执行到一个程序时如果遇到耗时的操作,那CPU就切换到另一个程序继续执行,等第一个程序的耗时操作执行完后再切换到第一个程序执行,这个切换过程不能只切换PC的指针,还需要记录一些其他的程序的信息,所以为了描述这种程序的信息,引入了进程的概念。 -狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。 -广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是[操作系统](https://baike.baidu.com/item/操作系统/192)动态执行的[基本单元](https://baike.baidu.com/item/基本单元),在传统的[操作系统](https://baike.baidu.com/item/操作系统)中,进程既是基本的[分配单元](https://baike.baidu.com/item/分配单元),也是基本的执行单元。 -进程的概念主要有两点:第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括[文本](https://baike.baidu.com/item/文本)区域(text region)、数据区域(data region)和[堆栈](https://baike.baidu.com/item/堆栈)(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有[处理](https://baike.baidu.com/item/处理)器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为[进程](https://baike.baidu.com/item/进程)。 +## 进程概念 +狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。 +广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是[操作系统](https://baike.baidu.com/item/操作系统/192)动态执行的[基本单元](https://baike.baidu.com/item/基本单元),在传统的[操作系统](https://baike.baidu.com/item/操作系统)中,进程既是基本的[分配单元](https://baike.baidu.com/item/分配单元),也是基本的执行单元。 + +进程的概念主要有两点:第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括[文本](https://baike.baidu.com/item/文本)区域(text region)、数据区域(data region)和[堆栈](https://baike.baidu.com/item/堆栈)(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有[处理](https://baike.baidu.com/item/处理)器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为[进程](https://baike.baidu.com/item/进程)。可以简单的理解为:进程 = 资源 + 指令执行序列。 进程是60年代初首先由[麻省理工学院](https://baike.baidu.com/item/麻省理工学院)的[MULTICS系统](https://baike.baidu.com/item/MULTICS系统)和IBM公司的[CTSS](https://baike.baidu.com/item/CTSS)/360系统引入的。 @@ -62,7 +66,7 @@ -## 进程的状态 +## 进程的状态(五状态进程模型) @@ -88,7 +92,9 @@ - 数据段:对应程序执行时所需要的数据部分,包括数据,堆栈和工作区。 - 进程控制块(Process Control Block, PCB) :描述进程的基本信息和运行状态,记录了进程运行时所需要的全部信息,它是进程存在的唯一标识,与进程一一对应。所谓的创建进程和撤销进程,都是指对PCB的操作。 -#### 进程控制块 +#### 进程控制块(PCB) + +又称进程描述符、进程属性,是操作系统用于管理进程的一个专门数据结构。PCB是操作系统感知进程存在的唯一标志。进程和PCB是一一对应的。 进程执行的任意时刻,都可由如下元素来表征: @@ -105,6 +111,8 @@ +进程表: 所有进程的PCB集合。 + ## 进程创建 操作系统决定创建一个新进程时,会按如下步骤操作: @@ -119,8 +127,24 @@ ## 进程切换 + + +### 进程表(Process Table) + 操作系统为了执行进程间的切换,会维护着一张表格,这张表就是进程表(process table)。每个进程占用一个进程表项。该表项包含了进程状态的重要信息,包括程序计数器、堆栈指针、内存分配状况、所打开文件的状态、账号和调度信息,以及其他在进程由运行态转换到就绪态或阻塞态时所必须保存的信息,从而保证该进程随后能再次启动,就像从未被中断过一样。 +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/process_table.png?raw=true) + +上面是典型的进程表表项中的一些字段: + +第一列内容与进程管理有关,第二列内容与存储管理有关,第三列内容与文件管理有关。 + + + + + + + **操作系统最底层的就是调度程序**,在它上面有许多进程。所有关于中断处理、启动进程和停止进程的具体细节都隐藏在调度程序中。事实上,调度程序只是一段非常小的程序。 表面上看,进程切换很简单。在某个时刻,操作系统中断一个正在运行的进程,将另一个进程置于运行模式,并把控制权交给后者。然而,这会引发若干个问题。首先,什么事件触发了进程的切换? 其次,必须认识到模式切换和进程切换键的区别。 @@ -171,31 +195,61 @@ -进程间的信息交换,具体内容分为:控制信息交换和数据交换,控制信息的交换为低级通信,数据的交换为高级通信。 +以Linux为例,进程间的信息交换,具体内容分为:控制信息交换和数据交换,控制信息的交换为低级通信,数据的交换为高级通信。 -- 信号 +- Socket套接字 + + socket 提供端到端的双相通信。一个套接字可以与一个或多个进程关联。就像管道有命令管道和未命名管道一样,套接字也有两种模式,套接字一般用于两个进程之间的网络通信,网络套接字需要来自诸如TCP(传输控制协议)或较低级别UDP(用户数据报协议)等基础协议的支持。 + +- Signals信号/信号量 + + 两个或多个进程可以通过简单的信号进行合作,可以强迫一个进程在某个位置停止,直到它接收到一个特定的信号。信号量是一个特殊的变量。用于进程间传递信息的一个整数值。 + + 信号是 UNIX 系统最先开始使用的进程间通信机制,因为 Linux 是继承于 UNIX 的,所以 Linux 也支持信号机制,通过向一个或多个进程发送`异步事件信号`来实现,信号可以从键盘或者访问不存在的位置等地方产生;信号通过 shell 将任务发送给子进程。 + + 你可以在 Linux 系统上输入 `kill -l` 来列出系统使用的信号。 + + 进程可以选择忽略发送过来的信号,但是有两个是不能忽略的:`SIGSTOP` 和 `SIGKILL` 信号。SIGSTOP 信号会通知当前正在运行的进程执行关闭操作,SIGKILL 信号会通知当前进程应该被杀死。除此之外,进程可以选择它想要处理的信号,进程也可以选择阻止信号,如果不阻止,可以选择自行处理,也可以选择进行内核处理。如果选择交给内核进行处理,那么就执行默认处理。 + + 操作系统会中断目标程序的进程来向其发送信号、在任何非原子指令中,执行都可以中断,如果进程已经注册了新号处理程序,那么就执行进程,如果没有注册,将采用默认处理的方式。 - 两个或多个进程可以通过简单的信号进行合作,可以强迫一个进程在某个位置停止,直到它接收到一个特定的信号。 +- Shared Memory共享内存 -- 共享存储系统 + 多台服务器访问同一个存储设备的同一分区。 - 多台服务器访问同一个存储设备的同一分区 + 两个进程之间还可以通过共享内存进行进程间通信,其中两个或者多个进程可以访问公共内存空间。两个进程的共享工作是通过共享内存完成的,一个进程所作的修改可以对另一个进程可见(很像线程间的通信)。 -- 消息传递系统 +- Message Queue消息传递系统/消息队列 进程与其它的进程进行通信而不必借助共享数据,通过互相发送和接收消息,建立一条通信链路。 -- 管道通信 + 一听到消息队列这个名词你可能不知道是什么意思,消息队列是用来描述内核寻址空间内的内部链接列表。可以按几种不同的方式将消息按顺序发送到队列并从队列中检索消息。每个消息队列由 IPC 标识符唯一标识。消息队列有两种模式,一种是`严格模式`, 严格模式就像是 FIFO 先入先出队列似的,消息顺序发送,顺序读取。还有一种模式是 `非严格模式`,消息的顺序性不是非常重要。 - 发送进程以字符流形式将大量数据送入管道,接收进程可从管道接收数据,二者利用管道进行通信,管道是单向的、先进先出的,它把一个进程的输出和另一个进程的输入连接在一起。一个进程(写进程)在管道的尾部写入数据,另一个进程(读进程)从管道的头部读出数据。每次只有一个进程能够真正地进入管道,其他的只能等待。 +- Pipe管道通信 + + 在两个进程之间,可以建立一个通道,一个进程向这个通道里写入字节流,另一个进程从这个管道中读取字节流。管道是同步的,当进程尝试从空管道读取数据时,该进程会被阻塞,直到有可用数据为止。shell 中的`管线 pipelines` 就是用管道实现的,当 shell 发现输出 + 发送进程以字符流形式将大量数据送入管道,接收进程可从管道接收数据,二者利用管道进行通信,管道是单向的、先进先出的,它把一个进程的输出和另一个进程的输入连接在一起。一个进程(写进程)在管道的尾部写入数据,另一个进程(读进程)从管道的头部读出数据。每次只有一个进程能够真正地进入管道,其他的只能等待。 + + + 管道分为无名管道和命名管道,前者用于父子进程通信,后者用于任意进程通信。 + + 入先出队列 FIFO 通常被称为 `命名管道(Named Pipes)`,命名管道的工作方式与常规管道非常相似,但是确实有一些明显的区别。未命名的管道没有备份文件:操作系统负责维护内存中的缓冲区,用来将字节从写入器传输到读取器。一旦写入或者输出终止的话,缓冲区将被回收,传输的数据会丢失。相比之下,命名管道具有支持文件和独特 API ,命名管道在文件系统中作为设备的专用文件存在。当所有的进程通信完成后,命名管道将保留在文件系统中以备后用。命名管道具有严格的 FIFO 行为,写入的第一个字节是读取的第一个字节,写入的第二个字节是读取的第二个字节,依此类推。 + + + + + + + + ## 进程调度 -进程调度就是处理器调度(上下文切换) +当一个计算机是多道程序设计系统时,会频繁的有很多进程或者线程来同时竞争 CPU 时间片。当两个或两个以上的进程/线程处于就绪状态时,就会发生这种情况。如果只有一个 CPU 可用,那么必须选择接下来哪个进程/线程可以运行。操作系统中有一个叫做调度程序(scheduler) 的角色存在,它就是做这件事儿的,该程序使用的算法叫做调度算法(scheduling algorithm) 。进程调度就是处理器调度(上下文切换)。 ### 调度级别 @@ -223,18 +277,56 @@ ### 调度算法 -- 先进先出 +- 先来先服务(first-come,first-serverd) 按照进入就绪队列的进程顺序,不加其他条件干涉 -- 短进程优先 +- 最短作业优先(Shortest Job First) 优先选出就绪队列中CPU执行时间最短的进程,例如:就绪队列有4个进程P1,P2,P3,P4,执行时间为:16,12,4,3 按照短进程优先,则周转时间(从进程提交到进程完成的时间间隔)分别为:35,19,7,3 - 平均周转时间:16,平均周转时间越小,调度性能越好 + 平均周转时间:16,平均周转时间越小,调度性能越好。 + + 在所有的进程都可以运行的情况下,最短作业优先的算法才是最优的。 + +- 最短剩余时间优先(Shortest Remaining Time Next) + + 使用这个算法,调度程序总是选择剩余运行时间最短的那个进程运行。 - 轮转法 - - 简单轮转: 就绪进程按FIFO排队,按照一定时间间隔让处理机分配给队列中的进程,就绪队列中**所有队列均可获得一个时间片的处理器运行** - - 多级队列: 让系统中所有进程分成若干类,每类一级 + + 一种最古老、最简单、最公平并且最广泛使用的算法就是轮询算法(round-robin)。每个进程都会被分配一个时间段,称为时间片(quantum),在这个时间片内允许进程运行。如果时间片结束时进程还在运行的话,则抢占一个 CPU 并将其分配给另一个进程。如果进程在时间片结束前阻塞或结束,则 CPU 立即进行切换。轮询算法比较容易实现。调度程序所做的就是维护一个可运行进程的列表,就像下图中的 a,当一个进程用完时间片后就被移到队列的末尾。 + + - 简单轮转: 就绪进程按FIFO排队,按照一定时间间隔让处理机分配给队列中的进程,就绪队列中所有队列均可获得一个时间片的处理器运行。 + - 多级队列: 让系统中所有进程分成若干类,每类一级。 + +- 优先级调度 + + 每个进程都被赋予一个优先级,优先级高的进程优先运行。 + + + + + +## 内核栈与用户栈的区别 + +每个进程一般会有两个栈,一个用户栈,一个内核栈,存在于内核空间。 + +当进程在用户空间运行时,cpu堆栈指针寄存器里面的内容时用户堆栈的地址,使用的是用户栈。 + +当进程在内核空间时,cpu堆栈指针寄存器里的内容是内核栈空间的地址,使用内核栈。 + +内核栈是**内存**中属于操作系统空间的一块区域,主要用途为: + +- 保护中断现场 +- 保护操作系统子程序间相互调用的参数、返回值、返回点以及子程序函数的局部变量 + +用户栈是用户进程空间中的一块区域,用于保存用户进程的子程序间相互作用的参数、返回值以及相关局部变量 + +**当进程因为中断或者系统调用而陷入内核态,进程所使用的堆栈也要从用户栈转到内核栈**。进程陷入内核态后,先把用户态堆栈的地址保存在内核栈中,然后设置堆栈指针寄存器的内容为内核栈的地址,这样就完成了用户栈到内核栈的转换。**当进程从内核态回复为用户态时,在内核态之后的最后将保存在内核栈里面的用户栈地址恢复到堆栈指针寄存器即可**。这样就实现了内核栈和用户栈的互转。 + +注意:**每次进程从用户态陷入内核的时候得到的内核栈都是空的,所以在进程陷入内核的时候,直接把内核栈的栈顶地址给堆栈指针寄存器即可。** + + ## 死锁 @@ -453,6 +545,111 @@ Android使用的进程有些不同。活动管理器是Android负责正在运行 多线程技术是指把执行一个应用程序的进程划分为可以同时运行的多个线程。 +为什么要有线程呢? + +在传统的操作系统中,每个进程都有一个地址空间和一个控制线程。事实上,这是大部分进程的定义。不过,在许多情况下,经常存在同一地址空间中运行多个控制线程的情形,这些线程就像是分离的进程。 + +上面说到进程 = 资源 + 指令执行序列。那能不能将资源和指令执行分开,组合成一个资源 + 多个指令执行序列的方式? 这种不切换资源,只切换执行指令的方式就是线程。线程保留了并发的优点,避免了进程切换的代价。 + + + +线程不像是进程那样具备较强的独立性。同一个进程中的所有线程都会有完全一样的地址空间,这意味着它们也共享同样的全局变量。由于每个线程都可以访问进程地址空间内每个内存地址,**「因此一个线程可以读取、写入甚至擦除另一个线程的堆栈」**。线程之间除了共享同一内存空间外,还具有如下不同的内容 + + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/process_thread_compare.png?raw=true) + +上图左边的是同一个进程中`每个线程共享`的内容,上图右边是`每个线程`中的内容。也就是说左边的列表是进程的属性,右边的列表是线程的属性。 + +**「线程之间的状态转换和进程之间的状态转换是一样的」**。 + +每个线程都会有自己的堆栈,如下图所示 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/thread_heap.png?raw=true) + +#### 线程系统调用 + +进程通常会从当前的某个单线程开始,然后这个线程通过调用一个库函数(比如 `thread_create`)创建新的线程。线程创建的函数会要求指定新创建线程的名称。创建的线程通常都返回一个线程标识符,该标识符就是新线程的名字。 + +当一个线程完成工作后,可以通过调用一个函数(比如 `thread_exit`)来退出。紧接着线程消失,状态变为终止,不能再进行调度。在某些线程的运行过程中,可以通过调用函数例如 `thread_join` ,表示一个线程可以等待另一个线程退出。这个过程阻塞调用线程直到等待特定的线程退出。在这种情况下,线程的创建和终止非常类似于进程的创建和终止。 + +另一个常见的线程是调用 `thread_yield`,它允许线程自动放弃 CPU 从而让另一个线程运行。这样一个调用还是很重要的,因为不同于进程,线程是无法利用时钟中断强制让线程让出 CPU 的。 + + + +### 线程实现 + +主要有三种实现方式 + +- 在用户空间中实现线程; +- 在内核空间中实现线程; +- 在用户和内核空间中混合实现线程。 + +下面我们分开讨论一下 + +#### 在用户空间中实现线程 + +第一种方法是把整个线程包放在用户空间中,内核对线程一无所知,它不知道线程的存在。所有的这类实现都有同样的通用结构 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/thread_user.png?raw=true) + +> `运行时系统(Runtime System)` 也叫做运行时环境,该运行时系统提供了程序在其中运行的环境。此环境可能会解决许多问题,包括应用程序内存的布局,程序如何访问变量,在过程之间传递参数的机制,与操作系统的接口等等。编译器根据特定的运行时系统进行假设以生成正确的代码。通常,运行时系统将负责设置和管理堆栈,并且会包含诸如垃圾收集,线程或语言内置的其他动态的功能。 + +在用户空间管理线程时,每个进程需要有其专用的`线程表(thread table)`,用来跟踪该进程中的线程。这些表和内核中的进程表类似,不过它仅仅记录各个线程的属性,如每个线程的程序计数器、堆栈指针、寄存器和状态。该线程表由运行时系统统一管理。当一个线程转换到就绪状态或阻塞状态时,在该线程表中存放重新启动该线程的所有信息,与内核在进程表中存放的信息完全一样。 + + + +在用户空间中实现线程要比在内核空间中实现线程具有这些方面的优势:考虑如果在线程完成时或者是在调用 `pthread_yield` 时,必要时会进程线程切换,然后线程的信息会被保存在运行时环境所提供的线程表中,然后,线程调度程序来选择另外一个需要运行的线程。保存线程的状态和调度程序都是`本地过程`,**所以启动他们比进行内核调用效率更高。因而不需要切换到内核,也就不需要上下文切换,也不需要对内存高速缓存进行刷新,因为线程调度非常便捷,因此效率比较高**。 + +在用户空间实现线程还有一个优势就是**它允许每个进程有自己定制的调度算法**。例如在某些应用程序中,那些具有垃圾收集线程的应用程序(知道是谁了吧)就不用担心自己线程会不会在不合适的时候停止,这是一个优势。用户线程还具有较好的可扩展性,因为内核空间中的内核线程需要一些表空间和堆栈空间,如果内核线程数量比较大,容易造成问题。 + + + +尽管在用户空间实现线程会具有一定的性能优势,但是劣势还是很明显的,你如何实现`阻塞系统调用`呢?假设在还没有任何键盘输入之前,一个线程读取键盘,让线程进行系统调用是不可能的,因为这会停止所有的线程。所以,**使用线程的一个目标是能够让线程进行阻塞调用,并且要避免被阻塞的线程影响其他线程**。 + +与阻塞调用类似的问题是`缺页中断`问题,实际上,计算机并不会把所有的程序都一次性的放入内存中,如果某个程序发生函数调用或者跳转指令到了一条不在内存的指令上,就会发生页面故障,而操作系统将到磁盘上取回这个丢失的指令,这就称为`缺页故障`。而在对所需的指令进行读入和执行时,相关的进程就会被阻塞。如果只有一个线程引起页面故障,内核由于甚至不知道有线程存在,通常会把整个进程阻塞直到磁盘 I/O 完成为止,尽管其他的线程是可以运行的。 + +另外一个问题是,如果一个线程开始运行,该线程所在进程中的其他线程都不能运行,除非第一个线程自愿的放弃 CPU,在一个单进程内部,没有时钟中断,所以不可能使用轮转调度的方式调度线程。除非其他线程能够以自己的意愿进入运行时环境,否则调度程序没有可以调度线程的机会。 + +### 在内核中实现线程 + +现在我们考虑使用内核来实现线程的情况,此时不再需要运行时环境了。另外,每个进程中也没有线程表。相反,在内核中会有用来记录系统中所有线程的线程表。当某个线程希望创建一个新线程或撤销一个已有线程时,它会进行一个系统调用,这个系统调用通过对线程表的更新来完成线程创建或销毁工作。 + +当某个线程希望创建一个新线程或撤销一个已有线程时,它会进行一个系统调用,这个系统调用通过对线程表的更新来完成线程创建或销毁工作。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/thread_kernel.png?raw=true) + +内核中的线程表持有每个线程的寄存器、状态和其他信息。这些信息和用户空间中的线程信息相同,但是位置却被放在了内核中而不是用户空间中。另外,内核还维护了一张进程表用来跟踪系统状态。 + +所有能够阻塞的调用都会通过系统调用的方式来实现,当一个线程阻塞时,内核可以进行选择,是运行在同一个进程中的另一个线程(如果有就绪线程的话)还是运行一个另一个进程中的线程。但是在用户实现中,运行时系统始终运行自己的线程,直到内核剥夺它的 CPU 时间片(或者没有可运行的线程存在了)为止。 + + + +由于在内核中创建或者销毁线程的开销比较大,所以某些系统会采用可循环利用的方式来回收线程。当某个线程被销毁时,就把它标志为不可运行的状态,但是其内部结构没有受到影响。稍后,在必须创建一个新线程时,就会重新启用旧线程,把它标志为可用状态。 + +如果某个进程中的线程造成缺页故障后,内核很容易的就能检查出来是否有其他可运行的线程,如果有的话,在等待所需要的页面从磁盘读入时,就选择一个可运行的线程运行。这样做的缺点是系统调用的代价比较大,所以如果线程的操作(创建、终止)比较多,就会带来很大的开销。 + + + +## 在用户和内核空间中混合实现线程 + +结合用户空间和内核空间的优点,设计人员采用了一种`内核级线程`的方式,然后将用户级线程与某些或者全部内核线程多路复用起来 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/user_kernel_thread_os.png?raw=true) + +在这种模型中,编程人员可以自由控制用户线程和内核线程的数量,具有很大的灵活度。采用这种方法,内核只识别内核级线程,并对其进行调度。其中一些内核级线程会被多个用户级线程多路复用。 + + + +**导致系统出现死锁的情况** + +死锁的出现需要同时满足下面四个条件 + +> - 互斥(Mutual Exclusion):一次只能有一个进程使用资源。如果另一个进程请求该资源,则必须延迟请求进程,直到释放该资源为止。 +> - 保持并等待(Hold and Wait):必须存在一个进程,该进程至少持有一个资源,并且正在等待获取其他进程当前所持有的资源。 +> - 无抢占(No Preemption):资源不能被抢占,也就是说,在进程完成其任务之后,只能由拥有它的进程自动释放资源。 +> - 循环等待(Circular Wait) :必须存在一组 {p0,p1,..... pn} 的等待进程,使 p0 等待 p1 持有的资源,p1 等待由 p2 持有的资源, pn-1 正在等待由 pn 持有的资源,而 pn 正在等待由 p0 持有的资源。 + ### 进程和线程的区别 @@ -467,7 +664,7 @@ Android使用的进程有些不同。活动管理器是Android负责正在运行 - 线程(thread):可分派的工作单元。它包括处理器上下文环境(包含程序计数器和栈指针)和栈中自身的数据区域。线程顺序执行且可以中断,因此处理器可以转到另一个线程。 - 进程(process):一个或多个线程和相关系统资源(如包含程序和代码的存储空间、打开的文件和设备)的集合。它严格对应于一个正在执行的程序的概念。通过把一个应用程序分解成多个线程,程序员可以很大程度上控制应用程序的模块性及相关事件的时间安排。 -线程比进程更轻量级,所以它们比进程更容易(更快)创建,也更容易撤销。在许多系统中,创建一个线程比创建一个进程要快10~100倍。 +线程比进程更轻量级,所以它们比进程更容易(更快)创建,也更容易撤销。在许多系统中,创建一个线程比创建一个进程要快10~100倍。线程会共享它所在进程的地址空间和其他资源。 diff --git "a/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" "b/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" index 33824b74..31ab0e09 100644 --- "a/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" +++ "b/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" @@ -34,6 +34,21 @@ 上述几种内存区域中数据段、BSS和堆通常是被连续存储的——内存位置上是连续的,而代码段和栈往往会被独立存放。有趣的是,堆和栈两个区域关系很“暧昧”,他们一个向下“长”(i386体系结构中栈向下、堆向上),一个向上“长”,相对而生。但你不必担心他们会碰头,因为他们之间间隔很大(到底大到多少,你可以从下面的例子程序计算一下),绝少有机会能碰到一起。 +## 地址空间 + +如果要使多个应用程序同时运行在内存中,必须要解决两个问题:`保护`和 `重定位`。第一种解决方式是用`保护密钥标记内存块`,并将执行过程的密钥与提取的每个存储字的密钥进行比较。这种方式只能解决第一种问题(破坏操作系统),但是不能解决多进程在内存中同时运行的问题。 + +还有一种更好的方式是创造一个存储器抽象:`地址空间(the address space)`。就像进程的概念创建了一种抽象的 CPU 来运行程序,地址空间也创建了一种抽象内存供程序使用。 + +#### 基址寄存器和变址寄存器 + +最简单的办法是使用`动态重定位(dynamic relocation)`技术,它就是通过一种简单的方式将每个进程的地址空间映射到物理内存的不同区域。还有一种方式是使用基址寄存器和变址寄存器。 + +- 基址寄存器:存储数据内存的起始位置 +- 变址寄存器:存储应用程序的长度。 + +每当进程引用内存以获取指令或读取、写入数据时,CPU 都会自动将`基址值`添加到进程生成的地址中,然后再将其发送到内存总线上。同时,它检查程序提供的地址是否大于或等于`变址寄存器` 中的值。如果程序提供的地址要超过变址寄存器的范围,那么会产生错误并中止访问。 + ## 内存的演变 @@ -90,7 +105,17 @@ Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自 ### 伙伴系统 -固定分区和动态分区方案都有缺陷。固定分区方案限制了活动进程数量,且如果可用分区的大小与进程大小很不匹配,那么内存空间的利用率会非常低。动态分区的维护特别复杂,并且会引入进行压缩的额外开销。更有吸引力的一种折中方案是伙伴系统。 +固定分区和动态分区方案都有缺陷。固定分区方案限制了活动进程数量,且如果可用分区的大小与进程大小很不匹配,那么内存空间的利用率会非常低。动态分区的维护特别复杂,并且会引入进行压缩的额外开销。更有吸引力的一种折中方案是伙伴系统。 它是一种经典的内存分配方案,是一种特殊的“分离适配”算法。主要思想是将内存按2的幂进行划分,组成若干空闲块链表,查找该链表找到能满足进程需求的最佳匹配块。 + +算法: + +- 首先将整个可用空间看做一块:2的幂,这里假设一共有1M的内存。 +- 假设进程申请的空间大小为s,这里假设为100k,如果满足2的u-1次幂 < s <= 2的u次幂,则分配整个块,否则,将块划分为两个大小相等的伙伴,大小为2的u-1次幂,那这两个大小相等的伙伴就是伙伴关系。等内存回收后,具有伙伴关系的两块还可以合并到一起,组成一个更大的内存块。 + - 1M内存分为两块。 + - 512k、512k仍然大于100k,将第一块再分为两块。 + - 256k、256k、512k,而第一块256k仍然大于100k,第一块256k再分配。 + - 128k、128k、256k、512k。这个时候64k < 100k < 128k,所以把第一个128k分配给当前申请100k内存的进程。 + - 等该进程的内存回收后,前两个128k的内存具有伙伴关系,还可以合并成256k。合并后和后面的256k又是一个伙伴,又合并成512k,发现和后面的512k又是一个伙伴,就又合并成了1M的内存块。 @@ -101,11 +126,13 @@ Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自 在大小相等的分区中一个进程在其声明周期中可能占据不同的分区。首次创建一个进程映像时,它被装入内存中的某个分区。以后该进程可能被换出,当它再次被换入时,可能被指定到与上一次不同的分区中。动态分区也存在同样的情况,压缩后内存中的进程也可能发生移动。因此,进程访问(指令和数据单元)的位置不是固定的。进程被换入或在内存中移动时,指令和数据单元的位置也发生变化。为了解决这个问题,需要区分几种地址类型: - 逻辑地址(logical address)是指与当前数据在内存中的物理分配地址无关的访问地址,在执行对内存的访问之前必须把它转换为物理地址。 -- 相对地址(relative address)是逻辑地址的一个特例,他是相对于某些已知点(通常是程序的开始处)的存储单元。 +- 相对地址(relative address)是逻辑地址的一个特例,他是相对于某些已知点(通常是程序的开始处)的存储单元。用户程序经过编译、汇编后形成目标代码,目标代码通常采用相对地址的形式,其首地址为0,其余地址相对于首地址而编址。 - 物理地址(physical address)或绝对地址是数据在内存中的实际位置。 系统采用运行时动态加载的方式把使用相对地址的程序加载到内存。通常情况下,被加载进程中所有内存访问都相对于程序的开始点。因此,在执行包括这类访问的指令时,需要有把相对地址转换为物理内存地址的硬件机制。这类地址转换就需要一个特殊的处理器寄存器(基址寄存器),其内容是程序在内存中的起始地址。还有一个界限寄存器指明程序的终止位置。 +将逻辑地址转换为物理地址的操作就叫做重定位。而把地址进行转换的功能部件就叫做内存管理单元(MMU, Memory Management Unit)。 + ### 分页 @@ -126,7 +153,7 @@ Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自 ### 分段 -细分用户程序的另一种可选方案是分段。采用分段技术,可以把程序和与其相关的数据划分到几个段(fragment)中。尽管段有最大长度限制,但并不要求所有程序的所有段的长度都相等。和分页一样,采用分段技术时的逻辑地址也是由两部分组成:段号和偏移量。 +细分用户程序的另一种可选方案是分段。采用分段技术,可以把用户进程地址空间按程序的自身的逻辑关系和与其相关的数据划分到几个段(fragment)中。尽管段有最大长度限制,但并不要求所有程序的所有段的长度都相等。同样内存空间也会被动态的划分为若干长度不相同的取悦,称为物理段,每个物理段由起始地址和长度确定。和分页一样,采用分段技术时的逻辑地址也是由两部分组成:段号和偏移量。 以段为单位进行分配,每段在内存中占据连续空间,但各段之间可以不相邻。 由于使用大小不等的段,分段类似于动态分区。在未采用覆盖方案或使用虚存的情况下,为执行一个程序,需要把它的所有段都装入内存。与动态分区不同的是,在分段方案中,一个程序可以占据多个分区,并且这些分区不要求是连续的。分段消除了内部碎片,但是和动态分区一样,他会产生外部碎片。不过由于进程被分成多个小块,因此外部碎片也会很小。 @@ -154,12 +181,22 @@ Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自 +### 段页式存储管理方案 + +综合页式、段式方案的优点,客服两者的缺点。用户进程先按段划分,每一段再按页面划分。内存分配还是以页为单位进行分配。 + + + + + ## 虚拟内存 面对越来越大的程序,常常产生程序>内存的问题,为解决这种问题,虚拟内存的概念得到普及. +虚拟内存是一种内存分配方案,是一项可以用来辅助内存分配的机制。我们知道,应用程序是按页装载进内存中的。但并不是所有的页都会装载到内存中,计算机中的硬件和软件会将数据从 RAM 临时传输到磁盘中来弥补内存的不足。如果没有虚拟内存的话,一旦你将计算机内存填满后,计算机会对你说呃,不,对不起,您无法再加载任何应用程序,请关闭另一个应用程序以加载新的应用程序。对于虚拟内存,计算机可以执行操作是查看内存中最近未使用过的区域,然后将其复制到硬盘上。虚拟内存通过复制技术实现了 妹子,你快来看哥哥能装这么多程序 的资本。复制是自动进行的,你无法感知到它的存在。 + 要有效的使用处理器和I/O设备,就需要在内存中保留尽可能多的进程。此外,还需要解除程序在开发时对程序使用内存大小的限制。解决这两个问题的途径就是虚拟内存技术。采用虚拟内存技术时,所有的地址访问都是逻辑访问,并在运行时转换为实地址。 虚拟内存机制使得期望运行大于物理内存的程序成为可能。其方法是将程序放在磁盘上,而将主存作为一种缓存,用来保存最频繁使用的部分程序。这种机制需要快速的映像内存地址,以便把程序生成的地址转换为有关字节在RAM中的物理地址。这种映像由CPU中的一个称为存储器管理单元(Memory Management Unit,MMU)的部件来完成。 @@ -211,7 +248,17 @@ Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自 以便处理在必须读取一个新页时,应该置换内存中的哪一页。当内存中的所有页框都被占据,且需要读取一个新页以处理一次缺页中断时,置换策略决定置换当前内存中的哪一页。所有策略的目标都是移出最近最不能访问的页。 +- `最优算法`在当前页面中置换最后要访问的页面。不幸的是,没有办法来判定哪个页面是最后一个要访问的,`因此实际上该算法不能使用`。然而,它可以作为衡量其他算法的标准。 +- `NRU` 算法根据 R 位和 M 位的状态将页面氛围四类。从编号最小的类别中随机选择一个页面。NRU 算法易于实现,但是性能不是很好。存在更好的算法。 +- `FIFO` 会跟踪页面加载进入内存中的顺序,并把页面放入一个链表中。有可能删除存在时间最长但是还在使用的页面,因此这个算法也不是一个很好的选择。 +- `第二次机会`算法是对 FIFO 的一个修改,它会在删除页面之前检查这个页面是否仍在使用。如果页面正在使用,就会进行保留。这个改进大大提高了性能。 +- `时钟` 算法是第二次机会算法的另外一种实现形式,时钟算法和第二次算法的性能差不多,但是会花费更少的时间来执行算法。 +- `LRU` 算法是一个非常优秀的算法,但是没有`特殊的硬件(TLB)`很难实现。如果没有硬件,就不能使用 LRU 算法。 +- `NFU` 算法是一种近似于 LRU 的算法,它的性能不是非常好。 +- `老化` 算法是一种更接近 LRU 算法的实现,并且可以更好的实现,因此是一个很好的选择 +- 最后两种算法都使用了工作集算法。工作集算法提供了合理的性能开销,但是它的实现比较复杂。`WSClock` 是另外一种变体,它不仅能够提供良好的性能,而且可以高效地实现。 +总之,**「最好的算法是老化算法和WSClock算法」**。他们分别是基于 LRU 和工作集算法。他们都具有良好的性能并且能够被有效的实现。还存在其他一些好的算法,但实际上这两个可能是最重要的。 #### 4. 驻留集管理 @@ -237,6 +284,16 @@ Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自 +## 内存映射 + +进程通过一个系统调用(mmap)将一个文件(或部分)映射到其虚拟地址空间的一部分,访问这个文件就像访问内存中的一个大数组,而不是对文件进行读写。 + +在多数实现中,在映射共享的页面时不会实际读入页面的内容,而是在访问页面时,页面才会被每次一页的读入(虚拟内存的缺页处理),磁盘文件则被当做后备存储。 + +当进程退出或显式地解除文件映射时,所有被修改页面会写回文件。 + + + ## Android内存管理 diff --git a/OperatingSystem/5.I:O.md b/OperatingSystem/5.I:O.md index 35535833..87a8fe44 100644 --- a/OperatingSystem/5.I:O.md +++ b/OperatingSystem/5.I:O.md @@ -29,6 +29,25 @@ +## I/O进程 + +I/O进程是专门处理系统中的I/O请求和I/O中断工作的。是系统进程,一般赋予最高优先级。一旦被唤醒,它可以很快抢占处理机投入运行。当I/O进程开始运行后,首先关闭中断,然后用receive去接收消息。如果没有消息,则开中断将自己堵塞,如果有消息,就去判断消息的类型是I/O请求还是I/O中断,然后再分别处理。 + +- I/O请求的进入 + + - 用户程序:调用send将I/O请求发送给I/O进程。调用block将自己堵塞,直到I/O任务完成后被唤醒。 + - 系统:利用wakeup唤醒I/O进程,完成用户所要求的I/O处理。 + +- I/O中断的进入 + + 当I/O中断发生时,内核中的中断处理程序发送一条消息给I/O进程,由I/O进程负责判断并处理中断。 + + ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/io_intercept_os.png?raw=true) + + 当一个 I/O 设备完成它的工作后,它就会产生一个中断(默认操作系统已经开启中断),它通过在总线上声明已分配的信号来实现此目的。主板上的中断控制器芯片会检测到这个信号,然后执行中断操作。 + +​ + ### 磁盘高速缓存 @@ -46,6 +65,58 @@ + + + + + + +每个设备控制器都会有一个应用程序与之对应,设备控制器通过应用程序的接口通过中断与操作系统进行通信。设备控制器是硬件,而设备驱动程序是软件。 + +### 内存映射 I/O + +每个控制器都会有几个寄存器用来和 CPU 进行通信。通过写入这些寄存器,操作系统可以命令设备发送数据,接收数据、开启或者关闭设备等。通过从这些寄存器中读取信息,操作系统能够知道设备的状态,是否准备接受一个新命令等。 + +为了控制`寄存器`,许多设备都会有`数据缓冲区(data buffer)`,来供系统进行读写。 + +那么问题来了,CPU 如何与设备寄存器和设备数据缓冲区进行通信呢?存在两个可选的方式。第一种方法是,每个控制寄存器都被分配一个 `I/O 端口(I/O port)`号,这是一个 8 位或 16 位的整数。所有 I/O 端口的集合形成了受保护的 I/O 端口空间,以便普通用户程序无法访问它(只有操作系统可以访问)。使用特殊的 I/O 指令像是 + +- + +``` +IN REG,PORT +``` + +CPU 可以读取控制寄存器 PORT 的内容并将结果放在 CPU 寄存器 REG 中。类似的,使用 + +- + +``` +OUT PORT,REG +``` + +CPU 可以将 REG 的内容写到控制寄存器中。大多数早期计算机,包括几乎所有大型主机,如 IBM 360 及其所有后续机型,都是以这种方式工作的。 + +第二个方法是 PDP-11 引入的,它将**「所有控制寄存器映射到内存空间」**中。 + +### 直接内存访问 + +无论一个 CPU 是否具有内存映射 I/O,它都需要寻址设备控制器以便与它们交换数据。CPU 可以从 I/O 控制器每次请求一个字节的数据,但是这么做会浪费 CPU 时间,所以经常会用到一种称为直接内存访问(Direct Memory Access) 的方案。它意味着 CPU 授予 I/O 模块权限在不涉及 CPU 的情况下读取或写入内存。也就是 DMA 可以不需要 CPU 的参与。这个过程由称为 DMA 控制器(DMAC)的芯片管理。由于 DMA 设备可以直接在内存之间传输数据,而不是使用 CPU 作为中介,因此可以缓解总线上的拥塞。DMA 通过允许 CPU 执行任务,同时 DMA 系统通过系统和内存总线传输数据来提高系统并发性。为了简化,我们假设 CPU 通过单一的系统总线访问所有的设备和内存,该总线连接 CPU 、内存和 I/O 设备,如下图所示 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/bus_os.png?raw=true) + +#### DMA 工作原理 + +首先 CPU 通过设置 DMA 控制器的寄存器对它进行编程,所以 DMA 控制器知道将什么数据传送到什么地方。DMA 控制器还要向磁盘控制器发出一个命令,通知它从磁盘读数据到其内部的缓冲区并检验校验和。当有效数据位于磁盘控制器的缓冲区中时,DMA 就可以开始了。 + +DMA 控制器通过在总线上发出一个`读请求`到磁盘控制器而发起 DMA 传送,这是第二步。这个读请求就像其他读请求一样,磁盘控制器并不知道或者并不关心它是来自 CPU 还是来自 DMA 控制器。通常情况下,要写的内存地址在总线的地址线上,所以当磁盘控制器去匹配下一个字时,它知道将该字写到什么地方。写到内存就是另外一个总线循环了,这是第三步。当写操作完成时,磁盘控制器在总线上发出一个应答信号到 DMA 控制器,这是第四步。 + +然后,DMA 控制器会增加内存地址并减少字节数量。如果字节数量仍然大于 0 ,就会循环步骤 2 - 步骤 4 ,直到字节计数变为 0 。此时,DMA 控制器会打断 CPU 并告诉它传输已经完成了。 + + + + + --- diff --git "a/OperatingSystem/6.\346\226\207\344\273\266\347\256\241\347\220\206.md" "b/OperatingSystem/6.\346\226\207\344\273\266\347\256\241\347\220\206.md" index 4940fb5d..e6908122 100644 --- "a/OperatingSystem/6.\346\226\207\344\273\266\347\256\241\347\220\206.md" +++ "b/OperatingSystem/6.\346\226\207\344\273\266\347\256\241\347\220\206.md" @@ -1,6 +1,8 @@ # 6.文件管理 +文件是对磁盘的抽象。 +所谓未见是指一组带标识(标识即为文件名)的、在逻辑上有完整意义的信息项的序列。 文件管理系统是一组系统软件,它为使用文件的用户和应用程序提供服务,包括文件访问、目录维护和访问控制。文件管理系统通常被视为一个由操作系统提供服务的系统服务,而不是操作系统的一部分,但是在任何系统中,至少有一部分文件管理功能是由操作系统执行的。 @@ -54,6 +56,78 @@ Android文件系统目录的顶层部分: +### 文件系统布局 + +文件系统存储在`磁盘`中。大部分的磁盘能够划分出一到多个分区,叫做`磁盘分区(disk partitioning)` 或者是`磁盘分片(disk slicing)`。每个分区都有独立的文件系统,每块分区的文件系统可以不同。磁盘的 0 号分区称为 `主引导记录(Master Boot Record, MBR)`,用来`引导(boot)` 计算机。在 MBR 的结尾是`分区表(partition table)`。每个分区表给出每个分区由开始到结束的地址。 + +当计算机开始引 boot 时,BIOS 读入并执行 MBR。 + +#### 引导块 + +MBR 做的第一件事就是`确定活动分区`,读入它的第一个块,称为`引导块(boot block)` 并执行。引导块中的程序将加载分区中的操作系统。为了一致性,每个分区都会从引导块开始,即使引导块不包含操作系统。引导块占据文件系统的前 4096 个字节,从磁盘上的字节偏移量 0 开始。引导块可用于启动操作系统。 + +除了从引导块开始之外,磁盘分区的布局是随着文件系统的不同而变化的。通常文件系统会包含一些属性,如下 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/file_system_os.png?raw=true) + +#### 超级块 + +紧跟在引导块后面的是 `超级块(Superblock)`,超级块 的大小为 4096 字节,从磁盘上的字节偏移 4096 开始。超级块包含文件系统的所有关键参数 + +- 文件系统的大小 +- 文件系统中的数据块数 +- 指示文件系统状态的标志 +- 分配组大小 + +在计算机启动或者文件系统首次使用时,超级块会被读入内存。 + +#### 空闲空间块 + +接着是文件系统中`空闲块`的信息,例如,可以用位图或者指针列表的形式给出。 + +#### 碎片 + +这里不得不提一个叫做`碎片(fragment)`的概念,也称为片段。一般零散的单个数据通常称为片段。磁盘块可以进一步分为固定大小的分配单元,片段只是在驱动器上彼此不相邻的文件片段。 + +#### inode + +然后在后面是一个 `inode(index node)`,也称作索引节点。它是一个数组的结构,每个文件有一个 inode,inode 非常重要,它说明了文件的方方面面。每个索引节点都存储对象数据的属性和磁盘块位置。 + +inode 节点主要包括了以下信息 + +- 模式/权限(保护) +- 所有者 ID +- 组 ID +- 文件大小 +- 文件的硬链接数 +- 上次访问时间 +- 最后修改时间 +- inode 上次修改时间 + +文件分为两部分,索引节点和块。一旦创建后,每种类型的块数是固定的。你不能增加分区上 inode 的数量,也不能增加磁盘块的数量。 + +紧跟在 inode 后面的是根目录,它存放的是文件系统目录树的根部。最后,磁盘的其他部分存放了其他所有的目录和文件。 + +### 文件的实现 + +最重要的问题是记录各个文件分别用到了哪些磁盘块。不同的系统采用了不同的方法。下面我们会探讨一下这些方式。分配背后的主要思想是`有效利用文件空间`和`快速访问文件` ,主要有三种分配方案 + +- 连续分配 +- 链表分配 +- 索引分配 + + + + + + + + + +- [参考: ](https://mp.weixin.qq.com/s/oLvTFWibCH53Fv5lj4mTnQ) + + + --- diff --git "a/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" "b/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" index 2f089353..11a00bf6 100644 --- "a/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" +++ "b/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" @@ -108,6 +108,8 @@ copy_to_user() //将数据从内核空间拷贝到用户空间 那就是让操作系统内核添加支持,传统的linux通信机制,比如socket、管道等都是内核的一部分,因此通过内核支持来实现进程间通信自然是没有问题的,但是binder并不是linux系统内核的一部分,那它是怎么做到访问内核空间的呢?这就得益于 Linux 的**动态内核可加载模块**(Loadable Kernel Module,LKM)的机制;模块是具有独立功能的程序,它可以被单独编译,但是不能独立运行。它在运行时被链接到内核作为内核的一部分运行。这样,Android 系统就可以通过动态添加一个内核模块运行在内核空间,用户进程之间通过这个内核模块作为桥梁来实现通信。 > 在 Android 系统中,这个运行在内核空间,负责各个用户进程通过 Binder 实现通信的内核模块就叫 **Binder 驱动**(Binder Dirver)(尽管名叫‘驱动’,实际上和硬件设备没有任何关系,只是实现方式和设备驱动程序是一样的)。 +> +> 从进程角度来看IPC机制,每个Android的进程,只能运行在自己进程所拥有的虚拟地址空间。对应一个4GB的虚拟地址空间,其中3GB是用户空间,1GB是内核空间,当然内核空间的大小是可以通过参数配置调整的。对于用户空间,不同进程之间彼此是不能共享的,而内核空间却是可共享的。Client进程向Server进程通信,恰恰是利用进程间可共享的内核内存空间来完成底层通信工作的,Client端与Server端进程往往采用ioctl等方法跟内核空间的驱动进行交互。 @@ -261,6 +263,8 @@ SystemServer和Zygote之间通信不使用Socket的原因是为了要解决fork - ServiceManager + ServiceManager是Binder IPC通信过程中的守护进程,本身也是一个Binder服务,但并没有采用libbinder中的多线程模型来与Binder驱动通信,而是自行编写了binder.c直接和Binder驱动来通信,并且只有一个循环binder_loop来进行读取和处理事务,这样的好处是简单而高效。ServiceManager是由init进程通过解析init.rc文件而创建的。 + ServiceManager本身的工作很简单:注册服务、查询服务、列出所有服务,启动一个死循环来解析Binder驱动读写动作,进行事务处理。ServiceManager用于管理系统中的各种服务。架构图如下所示: ![ServiceManager](http://gityuan.com/images/binder/prepare/IPC-Binder.jpg) @@ -276,10 +280,24 @@ SystemServer和Zygote之间通信不使用Socket的原因是为了要解决fork ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/binder_client_server.png) - + + +ServiceManger集中管理系统内的所有服务,通过权限控制进程是否有权注册服务,通过字符串名称来查找对应的Service; 由于ServiceManger进程建立跟所有向其注册服务的死亡通知, 那么当服务所在进程死亡后, 会只需告知ServiceManager. 每个Client通过查询ServiceManager可获取Server进程的情况,降低所有Client进程直接检测会导致负载过重。 +**ServiceManager启动流程:** +1. 打开binder驱动,并调用mmap()方法分配128k的内存映射空间:binder_open(); +2. 通知binder驱动使其成为守护进程:binder_become_context_manager(); +3. 验证selinux权限,判断进程是否有权注册或查看指定服务; +4. 进入循环状态,等待Client端的请求:binder_loop()。 +5. 注册服务的过程,根据服务名称,但同一个服务已注册,重新注册前会先移除之前的注册信息; +6. 死亡通知: 当binder所在进程死亡后,会调用binder_release方法,然后调用binder_node_release.这个过程便会发出死亡通知的回调. + +ServiceManager最核心的两个功能为查询和注册服务: + +- 注册服务:记录服务名和handle信息,保存到svclist列表; +- 查询服务:根据服务名查询相应的的handle信息。 ## Binder 通信过程 @@ -348,7 +366,39 @@ ARM64内存占用分配: ![img](https://img-blog.csdnimg.cn/20200329164637770.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3lpcmFuZmVuZw==,size_16,color_FFFFFF,t_70) -用户地址空间(服务端-数据接收端)和内核地址空间都映射到同一块物理地址空间。 +用户地址空间(服务端-数据接收端)和内核地址空间都映射到同一块物理地址空间,这也是Binder进程间通信效率高的核心机制所在。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/binder_physical_memory.jpg?raw=true) + +虚拟进程地址空间(vm_area_struct)和虚拟内核地址空间(vm_struct)都映射到同一块物理内存空间。当Client端与Server端发送数据时,Client(作为数据发送端)先从自己的进程空间把IPC通信数据`copy_from_user`拷贝到内核空间,而Server端(作为数据接收端)与内核共享数据,不再需要拷贝数据,而是通过内存地址空间的偏移量,即可获悉内存地址,整个过程只发生一次内存拷贝。一般地做法,需要Client端进程空间拷贝到内核空间,再由内核空间拷贝到Server进程空间,会发生两次拷贝。 + +对于进程和内核虚拟地址映射到同一个物理内存的操作是发生在数据接收端,而数据发送端还是需要将用户态的数据复制到内核态。到此,可能有读者会好奇,为何不直接让发送端和接收端直接映射到同一个物理空间,那样就连一次复制的操作都不需要了,0次复制操作那就与Linux标准内核的共享内存的IPC机制没有区别了,对于共享内存虽然效率高,但是对于多进程的同步问题比较复杂,而管道/消息队列等IPC需要复制2两次,效率较低。这里就不先展开讨论Linux现有的各种IPC机制跟Binder的详细对比,总之Android选择Binder的基于速度和安全性的考虑。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/binder_memory_map.jpg?raw=true) + + + +### Binder驱动 + +Binder驱动是Android专用的,但底层的驱动架构与Linux驱动一样。binder驱动在以misc设备进行注册,作为虚拟字符设备,没有直接操作硬件,只是对设备内存的处理。主要是驱动设备的初始化(binder_init),打开 (binder_open),映射(binder_mmap),数据操作(binder_ioctl),binder_ioctl()函数负责在两个进程间收发IPC数据和IPC reply数据。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/binder_driver.png?raw=true) + +### 系统调用 + +用户态的程序调用Kernel层驱动是需要陷入内核态,进行系统调用(`syscall`),比如打开Binder驱动方法的调用链为: open-> __open() -> binder_open()。 open()为用户空间的方法,__open()便是系统调用中相应的处理方法,通过查找,对应调用到内核binder驱动的binder_open()方法,至于其他的从用户态陷入内核态的流程也基本一致。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/binder_syscall.png?raw=true) + +简单说,当用户空间调用open()方法,最终会调用binder驱动的binder_open()方法;mmap()/ioctl()方法也是同理,在BInder系列的后续文章从用户态进入内核态,都依赖于系统调用过程。 + + + + + +在Android系统开机过程中,Zygote启动时会有一个[虚拟机注册过程](http://gityuan.com/2016/02/13/android-zygote/#jnistartreg),该过程调用AndroidRuntime::`startReg`方法来完成jni方法的注册。 + + diff --git "a/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/1.\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/1.\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" index 7af53a77..4e733af8 100644 --- "a/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/1.\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" +++ "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/1.\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" @@ -5,86 +5,81 @@ 基本概念 --- -- ***媒体***:是表示,传输,存储信息的载体,常人们见到的文字、声音、图像、图形等都是表示信息的媒体。 +- 媒体 -- ***多媒体***:是声音、动画、文字、图像和录像等各种媒体的组合,以图文并茂,生动活泼的动态形式表现出来,给人以很强的视觉冲击力,留下深刻印象. + 是表示,传输,存储信息的载体,常人们见到的文字、声音、图像、图形等都是表示信息的媒体。 -- ***多媒体技术***:是将文字、声音、图形、静态图像、动态图像与计算集成在一起的技术。它要解决的问题是计算机进一步帮助人类按最自然的和最习惯的方式接受和处理信息。 +- 多媒体 -- ***流媒体***:流媒体是指采用流式传输的方式在`Internet`播放的连续时基媒体格式,实际指的是一种新的媒体传送方式,而不是一种新的媒体格式(在网络上传输音/视频等多媒体信息现在主要有下载和流式传输两种方式)流式传输分两种方法:实时流式传输方式(`Realtime Streaming`)和顺序流式传输方式(`Progressive Streaming`)。 + 是声音、动画、文字、图像和录像等各种媒体的组合,以图文并茂,生动活泼的动态形式表现出来,给人以很强的视觉冲击力,留下深刻印象. -- ***多媒体文件***:是既包括视频又包括音频,甚至还带有脚本的一个集合,也可以叫容器。 +- 多媒体技术 -- ***媒体编码***:是文件当中的视频和音频所采用的压缩算法。也就是说一个`avi`的文件,当中的视频编码有可能是`A`,也可能是`B`,而其音频编码有可能是`1`,也有可能是`2`。 + 是将文字、声音、图形、静态图像、动态图像与计算集成在一起的技术。它要解决的问题是计算机进一步帮助人类按最自然的和最习惯的方式接受和处理信息。 -- ***转码***:指将一段多媒体包括音频、视频或者其他的内容从一种编码格式转换成为另外一种编码格式。 +- 流媒体 -- ***帧***:帧就是一段数据的组合,它是数据传输的基本单位。就是影像动画中最小单位的单幅影像画面,相当于电影胶片上的每一格镜头。一帧就是一副静止的画面,连续的帧就形成动画,如电视图像等。 + 流媒体是指采用流式传输的方式在`Internet`播放的连续时基媒体格式,实际指的是一种新的媒体传送方式,而不是一种新的媒体格式(在网络上传输音/视频等多媒体信息现在主要有下载和流式传输两种方式)流式传输分两种方法:实时流式传输方式(`Realtime Streaming`)和顺序流式传输方式(`Progressive Streaming`)。 -- ***视频***:连续的图象变化每秒超过24帧(`Frame`)画面以上时,根据视觉暂留原理,人眼无法辨别单幅的静态画面,看上去是平滑连续的视觉效果,这样连续的画面叫做视频. +- 多媒体文件 -- ***音频***:人类能听到的声音都成为音频,但是一般我们所说到的音频时存储在计算机里的声音。 + 既包括视频又包括音频,甚至还带有脚本的一个集合,也可以叫容器。 -- ***比特率(码率)***:码率就是数据传输时单位时间传送的数据位数,一般我们用的单位是`kbps`即千位(bit)每秒,也就是每秒钟传送多少个千位的信息。 通俗一点的理解就是取样率,单位时间内取样率越大,精度就越高,处理出来的文件就越接近原始文件,但是文件体积与取样率是成正比的,所以几乎所有的编码格式重视的都是如何用最低的码率达到最少的失真。但是因为编码算法不一样,所以也不能用码率来统一衡量音质或者画质.小写的b表示bit(位),大写的B表示byte(字节),一个字节=8个位,即1B=8b;前面的k表示1024的意思,即1024个位(Kb)或者1024个字节(KB),表示文件的大小单位,一般使用KB。1KB/s=8Kbps。码率(kbps)=文件大小(KB)*8/时间(秒)。 - - 动态码率(VBR: Variable Bit Rate) +- 媒体编码 - 比特率可以随着图像复杂程度的不同而随之变化。图像内容简单的片段采用较小的码率,图像 + 是文件当中的视频和音频所采用的压缩算法。也就是说一个`avi`的文件,当中的视频编码有可能是`A`,也可能是`B`,而其音频编码有可能是`1`,也有可能是`2`。 - 内容复杂的片段采用较大的码率,这样既保证了播放质量,又兼顾了数据量的限制。例如RMVB视频文件,其中的VB就是指VBR,表示采用动态比特率编码方式,达到播放质量与体积兼得的效果。 +- 转码 - - 静态比特率(CBR: Constant Bit Rate) + 指将一段多媒体包括音频、视频或者其他的内容从一种编码格式转换成为另外一种编码格式。 - 比特率恒定,图像内容复杂的片段质量不稳定,图像内容简单的片段质量较好。 - -- ***帧率(Frame Rate)***:帧/秒(`frames per second`)的缩写帧率即每秒显示帧数,帧率表示图形处理器处理场时每秒钟能够更新的次数。高的帧率可以得到更流畅、更逼真的动画。一般来说`30fps`就是可以接受的,但是将性能提升至`60fps`则可以明显提升交互感和逼真感,但是一般来说超过`75fps`一般就不容易察觉到有明显的流畅度提升了。如果帧率超过屏幕刷新率只会浪费图形处理的能力,因为监视器不能以这么快的速度更新,这样超过新率的帧率就浪费掉了。 - ![image](https://github.com/CharonChui/Pictures/blob/master/fps.gif?raw=true) +- 帧 -- ***关键帧***:相当于二维动画中的原画,指角色或者物体运动或变化中的关键动作所处的那一帧,它包含了图像的所有信息,后来帧仅包含了改变了的信息。如果你没有足够的关键帧,你的影片品质可能比较差,因为所有的帧从别的帧处产生。对于一般的用途,一个比较好的原则是每5秒设一个关键键。但如果时那种实时传输的流文件,那么要考虑传输网络的可靠度,所以要1到2秒增加一个关键帧。 + 帧就是一段数据的组合,它是数据传输的基本单位。就是影像动画中最小单位的单幅影像画面,相当于电影胶片上的每一格镜头。一帧就是一副静止的画面,连续的帧就形成动画,如电视图像等。 +- 视频 + 连续的图象变化每秒超过24帧(`Frame`)画面以上时,根据视觉暂留原理,人眼无法辨别单幅的静态画面,看上去是平滑连续的视觉效果,这样连续的画面叫做视频. +- 音频 -视频格式(封装格式/容器) ---- - + 人类能听到的声音都成为音频,但是一般我们所说到的音频时存储在计算机里的声音。 +- 比特率(码率) + +码率就是数据传输时单位时间传送的数据位数,一般我们用的单位是`kbps`即千位(bit)每秒,也就是每秒钟传送多少个千位的信息。 通俗一点的理解就是取样率,单位时间内取样率越大,精度就越高,处理出来的文件就越接近原始文件,但是文件体积与取样率是成正比的,所以几乎所有的编码格式重视的都是如何用最低的码率达到最少的失真。但是因为编码算法不一样,所以也不能用码率来统一衡量音质或者画质.小写的b表示bit(位),大写的B表示byte(字节),一个字节=8个位,即1B=8b;前面的k表示1024的意思,即1024个位(Kb)或者1024个字节(KB),表示文件的大小单位,一般使用KB。1KB/s=8Kbps。码率(kbps)=文件大小(KB)*8/时间(秒)。 + +- 动态码率(VBR: Variable Bit Rate) + + 比特率可以随着图像复杂程度的不同而随之变化。图像内容简单的片段采用较小的码率,图像 + + 内容复杂的片段采用较大的码率,这样既保证了播放质量,又兼顾了数据量的限制。例如RMVB视频文件,其中的VB就是指VBR,表示采用动态比特率编码方式,达到播放质量与体积兼得的效果。 + +- 静态比特率(CBR: Constant Bit Rate) + + 比特率恒定,图像内容复杂的片段质量不稳定,图像内容简单的片段质量较好。 + +- 帧率(Frame Rate) + +帧/秒(`frames per second`)的缩写帧率即每秒显示帧数,帧率表示图形处理器处理场时每秒钟能够更新的次数。高的帧率可以得到更流畅、更逼真的动画。一般来说`30fps`就是可以接受的,但是将性能提升至`60fps`则可以明显提升交互感和逼真感,但是一般来说超过`75fps`一般就不容易察觉到有明显的流畅度提升了。如果帧率超过屏幕刷新率只会浪费图形处理的能力,因为监视器不能以这么快的速度更新,这样超过新率的帧率就浪费掉了。 + ![image](https://github.com/CharonChui/Pictures/blob/master/fps.gif?raw=true) + +- 关键帧 -目前我们经常见的视频格式无非就是两大类: - -1. 影像格式(Video) -2. 流媒体格式(Stream Video) - -在影像格式中还可以根据出处划分为三大种: + 相当于二维动画中的原画,指角色或者物体运动或变化中的关键动作所处的那一帧,它包含了图像的所有信息,后来帧仅包含了改变了的信息。如果你没有足够的关键帧,你的影片品质可能比较差,因为所有的帧从别的帧处产生。对于一般的用途,一个比较好的原则是每5秒设一个关键键。但如果时那种实时传输的流文件,那么要考虑传输网络的可靠度,所以要1到2秒增加一个关键帧。 -1. `AVI`格式 - `AVI`英文全称为`Audio Video Interleaved`,即音频视频交错格式,是微软公司于1992年11月推出、作为其`Windows`视频软件一部分的一种多媒体容器格式。 - `AVI`文件将音频(语音)和视频(影像)数据包含在一个文件容器中,允许音视频同步回放。类似`DVD`视频格式,`AVI`文件支持多个音视频流。`AVI`信息主要应用在多媒体光盘上,用来保存电视、电影等各种影像信息。 - 其中数据块包含实际数据流,即图像和声音序列数据。这是文件的主体,也是决定文件容量的主要部分。视频文件的大小等于该文件的数据率乘以该视频播放的时间长度, - 索引块包括数据块列表和它们在文件中的位置,以提供文件内数据随机存取能力。文件头包括文件的通用信息,定义数据格式,所用的压缩算法等参数。 - `AVI`没有`MPEG`这么复杂,从`Windows 3.1`时代,它就已经面世了。它最直接的优点就是兼容好、调用方便而且图象质量好,因此也常常与`DVD`相并称。 - 但它的缺点也是十分明显的:体积大。也是因为这一点,我们才看到了`MPEG-1`和`MPEG-4`的诞生。2小时影像的`AVI`文件的体积与`MPEG-2`相差无几, - 不过这只是针对标准分辨率而言的:根据不同的应用要求,`AVI`的分辨率可以随意调。窗口越大,文件的数据量也就越大。降低分辨率可以大幅减低它的体积, - 但图象质量就必然受损。与`MPEG-2`格式文件体积差不多的情况下,`AVI`格式的视频质量相对而言要差不少,但制作起来对电脑的配置要求不高,经常有人先录制好了`AVI`格式的视频,再转换为其他格式。 -2. `MOV`格式 - `MOV`即`QuickTime`影片格式,它是`Apple`公司开发的一种音频、视频文件格式,用于存储常用数字媒体类型。当选择`QuickTime(*.mov)`作为“保存类型”时,动画将保存为`·mov`文件。`QuickTime`用于保存音频和视频信息,包括`Apple Mac OS,MicrosoftWindows95/98/NT/2003/XP/VISTA`,甚至`WINDOWS7`在内的所有主流电脑平台支持。`QuickTime`因具有跨平台、存储空间要求小等技术特点,而采用了有损压缩方式的`MOV`格式文件,画面效果较AVI格式要稍微好一些。到目前为止,它共有4个版本,其中以`4.0`版本的压缩率最好。这种编码支持16位图像深度的帧内压缩和帧间压缩,帧率每秒10帧以上。这种格式有些非编软件也可以对它实行处理,其中包括`ADOBE`公司的专业级多媒体视频处理软件`AFTEREFFECTS和PREMIERE`。 +- 刷新率 -3. `MPEG/MPG/DAT`: - 这是由国际标准化组织`ISO(International Standards Organization)`与`IEC(International Electronic Committee)`联合开发的一种编码视频格式。`MPEG`是运动图像压缩算法的国际标准,现已被几乎所有的计算机平台共同支持。MPEG也是`Motion Picture Experts Group`的缩写。这类格式包括了`MPEG-1, MPEG-2`和`MPEG-4`在内的多种视频格式。`MPEG-1`相信是大家接触得最多的了,因为目前其正在被广泛地应用在`VCD`的制作和一些视频片段下载的网络应用上面,大部分的`VCD`都是用 `MPEG1`格式压缩的( 刻录软件自动将`MPEG1`转为`.DAT`格式),使用`MPEG-1`的压缩算法,可以把一部120分钟长的电影压缩到1.2GB左右大小。`MPEG-2`则是应用在`DVD` 的制作,同时在一些`HDTV`(高清晰电视广播)和一些高要求视频编辑、处理上面也有相当多的应用。使用`MPEG-2`的压缩算法压缩一部120分钟长的电影可以压缩到5-8GB的大小(`MPEG2`的图像质量`MPEG-1`与其无法比拟的)。 + 刷新率是指屏幕每秒画面被刷新的次数,刷新率分为垂直刷新率和水平刷新率,一般提到的刷新率通常是指垂直刷新率。垂直刷新率表示屏幕上图像每秒重绘多少次,也就是每秒屏幕刷新的次数,以Hz(赫兹)为单位。刷新率越高,图像就越稳定,图像显示就越自然清晰,对眼镜的影响也越小。刷新率月底,图像闪烁和抖动的就越厉害,眼镜疲劳的越快。一般来说,如果能达到80Hz以上的刷新率,就可以完全消除图像闪烁和抖动感。 -在流媒体格式中同样还可以划分为三种: +- DTS -1. `RM`格式 - `Real Networks`公司所制定的音频/视频压缩规范`Real Media`中的一种,`Real Player`能做的就是利用`Internet`资源对这些符合`Real Media`技术规范的音频/视频进行实况转播。在`Real Media`规范中主要包括三类文件:`RealAudio`、`Real Video`和`Real Flash`(`Real Networks`公司与`Macromedia`公司合作推出的新一代高压缩比动画格式)。`REAL VIDEO`(RA、RAM)格式由一开始就是定位就是在视频流应用方面的,也可以说是视频流技术的始创者。它可以在用56K`MODEM`拨号上网的条件实现不间断的视频播放,从`RealVideo`的定位来看,就是牺牲画面质量来换取可连续观看性。其实`RealVideo`也可以实现不错的画面质量,由于`RealVideo`可以拥有非常高的压缩效率,很多人把`VCD`编码成`RealVideo`格式的,这样一来,一张光盘上可以存放好几部电影。`REAL VIDEO`存在颜色还原不准确的问题,`RealVideo`就不太适合专业的场合,但`RealVideo`出色的压缩效率和支持流式播放的特征,使得`RealVideo`在网络和娱乐场合占有不错的市场份额。 -2. `MOV/QT`格式 - `MOV`也可以作为一种流文件格式。`QuickTime`能够通过`Internet`提供实时的数字化信息流、工作流与文件回放功能,为了适应这一网络多媒体应用,`QuickTime`为多种流行的浏览器软件提供了相应的`QuickTime Viewer`插件,能够在浏览器中实现多媒体数据的实时回放。 -3. `ASF`格式 - `ASF`(`Advanced Streaming format`高级流格式)。`ASF`是`MICROSOFT`为了和现在的`Real player`竞争而发展出来的一种可以直接在网上观看视频节目的文件压缩格式。`ASF`使用了`MPEG4`的压缩算法,压缩率和图像的质量都很不错。因为`ASF`是以一个可以在网上即时观赏的视频“流”格式存在的,所以它的图像质量比`VCD`差一点点并不出奇,但比同是视频“流”格式的`RAM`格式要好。 `ASF`支持任意的压缩/解压缩编码方式,并可以使用任何一种底层网络传输协议,具有很大的灵活性。`ASF`流文件的数据速率可以在`28.8Kbps`到`3Mbps`之间变化。用户可以根据自己应用环境和网络条件选择一个合适的速率,实现`VOD`点播和直播。 + Decode Time Stamp,主要用于标识读入内存中的比特流在什么时候开始送入解码器中进行解码。 -4. `FLV`格式`FLV`是`FLASH VIDEO`的简称,一种流媒体封装格式,`FLV`流媒体格式是随着`Flash MX`的推出发展而来的视频格式。由于它形成的文件极小、加载速度极快,使得网络观看视频文件成为可能,它的出现有效地解决了视频文件导入`Flash`后,使导出的`SWF`文件体积庞大,不能在网络上很好的使用等问题。因此`FLV`格式成为了当今主流视频格式 +- PTS + Presentation Time Stamp,主要用于度量解码后的视频帧什么时候被显示出来。 -叫做容器很好理解,mp4就是一个容器,里面有moov文件表示文件的信息等,还有音频、画面等信息,他们统一到 -一起,放到一个容器里面,就组成了视频。如果觉得难以理解,可以想象成一瓶番茄酱。最外层的瓶子好比这个容器封装(Container),瓶子上注明的原材料和加工厂地等信息好比元信息(Metadata),瓶盖打开(解封装)后,番茄酱本身好比经过压缩处理过后的编码内容,番茄和调料加工成番茄酱的过程就好比编码(Codec),而原材料番茄和调料则好比最原本的内容元素(Content)。 音视频压缩编码标准 @@ -142,6 +137,53 @@ - `MP3`:(`MPEG-1 or MPEG-2 Audio Layer III`),是当曾经非常流行的一种数字音频编码和有损压缩格式,它被设计来大幅降低音频数据量。它是在1991 年,由位于德国埃尔朗根的研究组织`Fraunhofer-Gesellschaft`的一组工程师发明和标准化的。`MP3`的普及曾对音乐产业造成极大的冲击与影响。 - `WMA`:(`Windows Media Audio`)由微软公司开发的一种数字音频压缩格式,本身包括有损和无损压缩格式。 +--- + + + + +视频格式(封装格式/容器) +--- + +把编码后的音视频数据以一定格式封装到一个容器。 + +目前我们经常见的视频格式无非就是两大类: + +1. 影像格式(Video) +2. 流媒体格式(Stream Video) + +在影像格式中还可以根据出处划分为三大种: + +1. `AVI`格式 + `AVI`英文全称为`Audio Video Interleaved`,即音频视频交错格式,是微软公司于1992年11月推出、作为其`Windows`视频软件一部分的一种多媒体容器格式。 + `AVI`文件将音频(语音)和视频(影像)数据包含在一个文件容器中,允许音视频同步回放。类似`DVD`视频格式,`AVI`文件支持多个音视频流。`AVI`信息主要应用在多媒体光盘上,用来保存电视、电影等各种影像信息。 + 其中数据块包含实际数据流,即图像和声音序列数据。这是文件的主体,也是决定文件容量的主要部分。视频文件的大小等于该文件的数据率乘以该视频播放的时间长度, + 索引块包括数据块列表和它们在文件中的位置,以提供文件内数据随机存取能力。文件头包括文件的通用信息,定义数据格式,所用的压缩算法等参数。 + `AVI`没有`MPEG`这么复杂,从`Windows 3.1`时代,它就已经面世了。它最直接的优点就是兼容好、调用方便而且图象质量好,因此也常常与`DVD`相并称。 + 但它的缺点也是十分明显的:体积大。也是因为这一点,我们才看到了`MPEG-1`和`MPEG-4`的诞生。2小时影像的`AVI`文件的体积与`MPEG-2`相差无几, + 不过这只是针对标准分辨率而言的:根据不同的应用要求,`AVI`的分辨率可以随意调。窗口越大,文件的数据量也就越大。降低分辨率可以大幅减低它的体积, + 但图象质量就必然受损。与`MPEG-2`格式文件体积差不多的情况下,`AVI`格式的视频质量相对而言要差不少,但制作起来对电脑的配置要求不高,经常有人先录制好了`AVI`格式的视频,再转换为其他格式。 +2. `MOV`格式 + `MOV`即`QuickTime`影片格式,它是`Apple`公司开发的一种音频、视频文件格式,用于存储常用数字媒体类型。当选择`QuickTime(*.mov)`作为“保存类型”时,动画将保存为`·mov`文件。`QuickTime`用于保存音频和视频信息,包括`Apple Mac OS,MicrosoftWindows95/98/NT/2003/XP/VISTA`,甚至`WINDOWS7`在内的所有主流电脑平台支持。`QuickTime`因具有跨平台、存储空间要求小等技术特点,而采用了有损压缩方式的`MOV`格式文件,画面效果较AVI格式要稍微好一些。到目前为止,它共有4个版本,其中以`4.0`版本的压缩率最好。这种编码支持16位图像深度的帧内压缩和帧间压缩,帧率每秒10帧以上。这种格式有些非编软件也可以对它实行处理,其中包括`ADOBE`公司的专业级多媒体视频处理软件`AFTEREFFECTS和PREMIERE`。 + +3. `MPEG/MPG/DAT`: + 这是由国际标准化组织`ISO(International Standards Organization)`与`IEC(International Electronic Committee)`联合开发的一种编码视频格式。`MPEG`是运动图像压缩算法的国际标准,现已被几乎所有的计算机平台共同支持。MPEG也是`Motion Picture Experts Group`的缩写。这类格式包括了`MPEG-1, MPEG-2`和`MPEG-4`在内的多种视频格式。`MPEG-1`相信是大家接触得最多的了,因为目前其正在被广泛地应用在`VCD`的制作和一些视频片段下载的网络应用上面,大部分的`VCD`都是用 `MPEG1`格式压缩的( 刻录软件自动将`MPEG1`转为`.DAT`格式),使用`MPEG-1`的压缩算法,可以把一部120分钟长的电影压缩到1.2GB左右大小。`MPEG-2`则是应用在`DVD` 的制作,同时在一些`HDTV`(高清晰电视广播)和一些高要求视频编辑、处理上面也有相当多的应用。使用`MPEG-2`的压缩算法压缩一部120分钟长的电影可以压缩到5-8GB的大小(`MPEG2`的图像质量`MPEG-1`与其无法比拟的)。 + +在流媒体格式中同样还可以划分为三种: + +1. `RM`格式 + `Real Networks`公司所制定的音频/视频压缩规范`Real Media`中的一种,`Real Player`能做的就是利用`Internet`资源对这些符合`Real Media`技术规范的音频/视频进行实况转播。在`Real Media`规范中主要包括三类文件:`RealAudio`、`Real Video`和`Real Flash`(`Real Networks`公司与`Macromedia`公司合作推出的新一代高压缩比动画格式)。`REAL VIDEO`(RA、RAM)格式由一开始就是定位就是在视频流应用方面的,也可以说是视频流技术的始创者。它可以在用56K`MODEM`拨号上网的条件实现不间断的视频播放,从`RealVideo`的定位来看,就是牺牲画面质量来换取可连续观看性。其实`RealVideo`也可以实现不错的画面质量,由于`RealVideo`可以拥有非常高的压缩效率,很多人把`VCD`编码成`RealVideo`格式的,这样一来,一张光盘上可以存放好几部电影。`REAL VIDEO`存在颜色还原不准确的问题,`RealVideo`就不太适合专业的场合,但`RealVideo`出色的压缩效率和支持流式播放的特征,使得`RealVideo`在网络和娱乐场合占有不错的市场份额。 +2. `MOV/QT`格式 + `MOV`也可以作为一种流文件格式。`QuickTime`能够通过`Internet`提供实时的数字化信息流、工作流与文件回放功能,为了适应这一网络多媒体应用,`QuickTime`为多种流行的浏览器软件提供了相应的`QuickTime Viewer`插件,能够在浏览器中实现多媒体数据的实时回放。 +3. `ASF`格式 + `ASF`(`Advanced Streaming format`高级流格式)。`ASF`是`MICROSOFT`为了和现在的`Real player`竞争而发展出来的一种可以直接在网上观看视频节目的文件压缩格式。`ASF`使用了`MPEG4`的压缩算法,压缩率和图像的质量都很不错。因为`ASF`是以一个可以在网上即时观赏的视频“流”格式存在的,所以它的图像质量比`VCD`差一点点并不出奇,但比同是视频“流”格式的`RAM`格式要好。 `ASF`支持任意的压缩/解压缩编码方式,并可以使用任何一种底层网络传输协议,具有很大的灵活性。`ASF`流文件的数据速率可以在`28.8Kbps`到`3Mbps`之间变化。用户可以根据自己应用环境和网络条件选择一个合适的速率,实现`VOD`点播和直播。 + +4. `FLV`格式`FLV`是`FLASH VIDEO`的简称,一种流媒体封装格式,`FLV`流媒体格式是随着`Flash MX`的推出发展而来的视频格式。由于它形成的文件极小、加载速度极快,使得网络观看视频文件成为可能,它的出现有效地解决了视频文件导入`Flash`后,使导出的`SWF`文件体积庞大,不能在网络上很好的使用等问题。因此`FLV`格式成为了当今主流视频格式 + + +叫做容器很好理解,mp4就是一个容器,里面有moov文件表示文件的信息等,还有音频、画面等信息,他们统一到 +一起,放到一个容器里面,就组成了视频。如果觉得难以理解,可以想象成一瓶番茄酱。最外层的瓶子好比这个容器封装(Container),瓶子上注明的原材料和加工厂地等信息好比元信息(Metadata),瓶盖打开(解封装)后,番茄酱本身好比经过压缩处理过后的编码内容,番茄和调料加工成番茄酱的过程就好比编码(Codec),而原材料番茄和调料则好比最原本的内容元素(Content)。 + 一些音视频的参数含义 --- diff --git "a/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/2.\347\263\273\347\273\237\346\222\255\346\224\276\345\231\250MediaPlayer.md" "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/2.\347\263\273\347\273\237\346\222\255\346\224\276\345\231\250MediaPlayer.md" new file mode 100644 index 00000000..405341cd --- /dev/null +++ "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/2.\347\263\273\347\273\237\346\222\255\346\224\276\345\231\250MediaPlayer.md" @@ -0,0 +1,30 @@ +2.系统播放器MediaPlayer +=== + + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/mediaplayer_state.png?raw=true) + + + + + + + + + + + + + + +相关知识: + +- [封装格式](https://en.wikipedia.org/wiki/Comparison_of_video_container_formats) +- [视频编码方式](https://en.wikipedia.org/wiki/Comparison_of_video_codecs) +- [音频编码方式](https://en.wikipedia.org/wiki/Comparison_of_audio_coding_formats) + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file From 02d97cc227dea999e4d81d7a44debf14a4c501c5 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 18 Feb 2021 15:39:35 +0800 Subject: [PATCH 044/213] update --- ...13\344\270\216\347\272\277\347\250\213.md" | 4 +- ...13\351\227\264\351\200\232\344\277\241.md" | 41 +++++++++++++++++ ...30\345\210\266\345\237\272\347\241\200.md" | 44 +++++++++++++++++++ ...ManagerService\347\256\200\344\273\213.md" | 4 ++ ...57\345\212\250\350\277\207\347\250\213.md" | 10 ++++- 5 files changed, 99 insertions(+), 4 deletions(-) diff --git "a/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" index c96b8f66..9801b918 100644 --- "a/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" +++ "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" @@ -293,7 +293,7 @@ 使用这个算法,调度程序总是选择剩余运行时间最短的那个进程运行。 - 轮转法 - + 一种最古老、最简单、最公平并且最广泛使用的算法就是轮询算法(round-robin)。每个进程都会被分配一个时间段,称为时间片(quantum),在这个时间片内允许进程运行。如果时间片结束时进程还在运行的话,则抢占一个 CPU 并将其分配给另一个进程。如果进程在时间片结束前阻塞或结束,则 CPU 立即进行切换。轮询算法比较容易实现。调度程序所做的就是维护一个可运行进程的列表,就像下图中的 a,当一个进程用完时间片后就被移到队列的末尾。 - 简单轮转: 就绪进程按FIFO排队,按照一定时间间隔让处理机分配给队列中的进程,就绪队列中所有队列均可获得一个时间片的处理器运行。 @@ -402,7 +402,7 @@ Linux为进程间通信和同步提供了各种机制。这里只是几种。 ## Android进程结构 -如同传统的Linux系统一样,Android的第一个用户空间进程是init,它是所有其他进程的根。然而,Android的init启动的守护进程是不同的,这些守护进程更多的聚焦于底层细节(管理文件系统和硬件访问),而不是高层用户设施,例如调度定时任务。Android还有一层额外的进程,它们运行Dalvik的Java语言环境,负责执行系统中所有以Java实现的部分。 +如同传统的Linux系统一样,Android的第一个用户空间进程是init(init的PID值是0),它是所有其他进程的根。然而,Android的init启动的守护进程是不同的,这些守护进程更多的聚焦于底层细节(管理文件系统和硬件访问),而不是高层用户设施,例如调度定时任务。Android还有一层额外的进程,它们运行Dalvik的Java语言环境,负责执行系统中所有以Java实现的部分。 diff --git "a/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" "b/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" index 11a00bf6..48a912cc 100644 --- "a/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" +++ "b/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" @@ -221,6 +221,47 @@ SystemServer和Zygote之间通信不使用Socket的原因是为了要解决fork 在Android系统的Binder机制中,由一系统组件组成,分别是Client、Server、Service Manager和Binder驱动程序,其中Client、Server和Service Manager运行在用户空间,Binder驱动程序运行内核空间。其中Service Manager和Binder驱动由系统提供,而Client、Server由应用程序来实现。其中,核心组件便是Binder驱动程序了,Service Manager提供了辅助管理的功能,Client和Server正是在Binder驱动和Service Manager提供的基础设施上,进行Client-Server之间的通信。Client、Server 和 ServiceManager 均是通过系统调用 open、mmap 和 ioctl 来访问设备文件 /dev/binder,从而实现与Binder驱动的交互来间接的实现跨进程通信。 +如果统观Binder中的各个组成元素,就会惊奇的发现它和TCP/IP网络有很多相似之处: + +- Binder驱动 -> 路由器 +- Service Manager -> DNS +- Binder Client -> 客户端 +- Binder Server -> 服务端 + +TCP/IP中一个典型的服务连接过程(比如客户端通过浏览器方位Google主页): + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/binder_tcp_ip.png) + +在这个简化的流程图中共出现了四种角色,即Client、Server、DNS和Router。它们的目标是让Client和Server建立连接,主要分为如下几个步骤。 + +- Client向DNS查询Google.com的IP地址。 + + 显然,Client一定要先知道DNS的IP地址,才有可能向它发起查询。DNS的IP设置是在客户端接入网络前就完成了的,否则Client将无法正常访问域名服务器。当然,如果Client已经知晓了Server的IP,那么完全可以跨越这一步而直接与Server连接。比如Windows操作系统就提供了一个hosts文件,用于查询常用网址域名与其IP地址的对应关系。当用户需要访问某个网址时,系统会先从这个文件中判断是否已经存在有这个域名的对应IP。如果有就不需要再大费周折地向DNS查询了,从而加快访问速度。 + +- DNS将查询结果返回Client。 + + 因为Client的IP地址对于DNS也必须是可知的,这些信息都会被封装在TCP/IP包中。 + +- Client发起连接。 + +- Client在得到Google.com的IP地址后,就可以据此来向Google服务器发起连接了。 + +在这一些列流程中,我们并没有特别提及Router的作用。因为它所担负的责任是将数据包投递到用户设定的目标IP中,即Router是整个通信结构中的基础。 + +从这个典型的TCP/IP通信中,我们还能得到什么提示呢? + +首先,在TCP/IP参考模型中,对于IP层及以上的用户来说,IP地址是他们彼此沟通的凭证,任何用户在整个互联网中的IP标志符都是唯一的。 + +其次,Router是构建一个通信网络的基础,它可以根据用户填写的目标IP正确的把数据包发送到位。 + +最后DNS角色并不是必需的,它的出现是为了帮助人们使复杂难记的IP地址与可读性更强的域名建立关联,并提供查询功能。而客户端能使用DNS的前提是它已经正确配置了DNS服务器的IP地址。 + +Binder的本质目标用一句话来描述,就是进程1(客户端)希望与进程2(服务端)进行互访。但因为它们之间是跨进程(跨网络)的,所以必须借助于Binder驱动(路由器)来把请求正确投递到对方所在进程(网络)中。而参与通信的进程们需要持有Binder“颁发”的唯一标志(IP地址)。和TCP/IP网络类似,Binder中的DNS也并不是必需的,前提是客户端能记住它要访问的进程的Binder标志(IP地址)。而且要特别注意这个标志是动态IP,这就意味着即使客户端记住了本次通信过程中目标进程的唯一标志,下一次访问仍然需要重新获取,这无疑加大了客户端的难度。DNS的出现可以完美的解决这个问题,用于管理Binder标志与可读性更强的域名间的对应关系,并向用户提供查询功能。既然Service Manager是DNS,那么它的IP地址是什么呢? Binder机制对此作了特别规定,Service Manager在Binder通信过程中的唯一标志永远都是0. + + + + + ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/binder_framework.png) - Server diff --git "a/OperatingSystem/AndroidKernal/6.\345\261\217\345\271\225\347\273\230\345\210\266\345\237\272\347\241\200.md" "b/OperatingSystem/AndroidKernal/6.\345\261\217\345\271\225\347\273\230\345\210\266\345\237\272\347\241\200.md" index 95e4f084..620d6f10 100644 --- "a/OperatingSystem/AndroidKernal/6.\345\261\217\345\271\225\347\273\230\345\210\266\345\237\272\347\241\200.md" +++ "b/OperatingSystem/AndroidKernal/6.\345\261\217\345\271\225\347\273\230\345\210\266\345\237\272\347\241\200.md" @@ -39,6 +39,50 @@ Android的屏幕绘制架构如下图: - 每个具体的Drawable对象仅仅绘制某个特定的图案,SDK中包含的Drawable实现类有BitmapDrawable、NinePatchDrawable等。 + +Linux内核提供了统一的framebuffer显示驱动。设备节点/dev/graphics/fb*或者/dev/fb*,其中fb0表示第一个Monitor,当前系统中只用到了一个显示屏。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_surface_flinger.png) + +Android的HAL层提供了Gralloc,包括fb和gralloc两个设备。前者负责打开内核中的framebuffer、初始化配置,并提供了post、setSwapInterval等操作接口。后者则管理帧缓冲区的分配和释放。这就意味着上层元素只能通过Gralloc来间接访问帧缓冲区,从而保证了系统对framebuffer的有序使用和统一管理。 + +另外,HAL层的另一重要模块是“Composer”,如其名所示,它为厂商自定制“UI合成”提供了接口。Composer的直接使用者是SurfaceFlinger中的HWComposer,后者除了管理Composer的HAL模块外,还负责VSync信号的产生和控制。VSync则是Project Butter工程中加入的一种同步机制,它既可以由硬件产生,也可以通过软件来moni(VsyncThread)。 + + + +Framebuffer是系内核系统提供的图形硬件的抽象描述。之所以称为buffer,是因为它也占用了系统存储空间的一部分,是一块包含屏幕显示信息的缓冲区。由此可见,在一切都是文件的Linux系统中,Framebuffer被看成了终端显示设备的化身。 + +另外,Framebuffer借助于Linux文件系统向上层应用提供了统一而高效的操作接口,从而让用户空间中运行的程序可以在不做太多修改的情况下去适配多种显示设备,无论它们属于哪家厂商、什么型号,都由Framebuffer内部来兼容。 + +在Android系统的GUI设计理念中,提供了两种本地窗口: + +- 面向管理者(SurfaceFlinger) + + 既然SurfaceFlinger扮演了系统中所有UI界面的管理者,那么它无可厚非需要直接或间接地持有“本地窗口。这个窗口就是FramebufferNativeWindow。 + +- 面向应用程序 + + 这类本地窗口是Surface。 + + + + + + + + + + + + + + + + + + + + --- diff --git "a/OperatingSystem/AndroidKernal/8.WindowManagerService\347\256\200\344\273\213.md" "b/OperatingSystem/AndroidKernal/8.WindowManagerService\347\256\200\344\273\213.md" index cb3820e1..949eaffd 100644 --- "a/OperatingSystem/AndroidKernal/8.WindowManagerService\347\256\200\344\273\213.md" +++ "b/OperatingSystem/AndroidKernal/8.WindowManagerService\347\256\200\344\273\213.md" @@ -13,6 +13,10 @@ WmS是Android中图形用户接口的引擎,它管理着所有窗口,包括 - 保持窗口的层次关系,以便SurfaceFlinger能够据此绘制屏幕 - 把窗口信息传递给InputManager对象,以便InputDispatcher能够把输入消息派发给和屏幕上显示一致的窗口。 + + +打个比方,就像一出由N个演员参与的话剧:SurfaceFlinger是摄像机,WMS是导演,ViewRoot则是演员个体。摄像机(SurfaceFlinger)的作用是单一而规范的,它负责客观地捕获当前的画面,然后真是的呈现给观众。导演(WMS)则会考虑到话剧的舞台效果和视觉美感,如他需要根据实际情况来安排各个演员的排序站位,谁在前谁在后,都会影响到演出的”画面效果“与”剧情编排“,而各个演员的长相和标清(ViewRoot)则更多的取决于他们自身的条件与努力。正式通过这三者的”各司其职“,才能最终为观众呈现出异常美妙绝伦的”视觉盛宴“。 + Android采用层叠式布局,这种布局的特点在于允许多个窗口层叠显示。该布局一般都需要一个窗口管理服务端。从程序设计的角度看,有两种设计模式可以实现服务端: - 独立进程方式 diff --git "a/SourceAnalysis/Activity\345\220\257\345\212\250\350\277\207\347\250\213.md" "b/SourceAnalysis/Activity\345\220\257\345\212\250\350\277\207\347\250\213.md" index cc1cdbb3..5c0d5d6c 100644 --- "a/SourceAnalysis/Activity\345\220\257\345\212\250\350\277\207\347\250\213.md" +++ "b/SourceAnalysis/Activity\345\220\257\345\212\250\350\277\207\347\250\213.md" @@ -1496,9 +1496,15 @@ final void startActivityLocked(ActivityRecord r, boolean newTask, 在Android应用程序框架层中,是由ActivityManagerService组件负责为Android应用程序创建新的进程的,它本来也是运行在一个独立的进程之中,不过这个进程是在系统启动的过程中创建的。 ActivityManagerService组件一般会在什么情况下会为应用程序创建一个新的进程呢?当系统决定要在一个新的进程中启动一个Activity或者Service时,它就会创建一个新的进程了, - 然后在这个新的进程中启动这个Activity或者Service + 然后在这个新的进程中启动这个Activity或者Service。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/ams_start_activity.png) + +无论以什么方式发起一个Activity的启动流程,最终都会调用到AMS的startActivity的函数。而在AMS真正启动一个Activity之前,需要经过众多烦琐的判断和准备工作,这些工作在AMS内部都是由一些列以startActivity开头的函数来进行逐步处理的。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/activity_start_binder.png) + - --- - 邮箱 :charon.chui@gmail.com From fead41f580ac363bd0004af47851c622ba5a5530 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 15 Mar 2021 14:42:49 +0800 Subject: [PATCH 045/213] update --- .../\345\205\263\351\224\256\345\270\247.md" | 33 +++++- ...47\350\203\275\344\274\230\345\214\226.md" | 14 +++ .../TS.md" | 104 +++++++++++++++++- ...01\350\243\205\346\240\274\345\274\217.md" | 8 ++ 4 files changed, 152 insertions(+), 7 deletions(-) diff --git "a/VideoDevelopment/\345\205\263\351\224\256\345\270\247.md" "b/VideoDevelopment/\345\205\263\351\224\256\345\270\247.md" index da3274dc..5366ba8d 100644 --- "a/VideoDevelopment/\345\205\263\351\224\256\345\270\247.md" +++ "b/VideoDevelopment/\345\205\263\351\224\256\345\270\247.md" @@ -1,21 +1,42 @@ 关键帧 === -**编码器将多张图像进行编码后生产成一段一段的 GOP ( Group of Pictures ) , 解码器在播放时则是读取一段一段的 GOP 进行解码后读取画面再渲染显示。**GOP ( Group of Pictures) 是一组连续的画面,由一张 I 帧和数张 B / P 帧组成,是视频图像编码器和解码器存取的基本单位,它的排列顺序将会一直重复到影像结束。I 帧是内部编码帧(也称为关键帧),P帧是前向预测帧(前向参考帧),B 帧是双向内插帧(双向参考帧)。简单地讲,I 帧是一个完整的画面,而 P 帧和 B 帧记录的是相对于 I 帧的变化。如果没有 I 帧,P 帧和 B 帧就无法解码。 -在H.264压缩标准中I帧、P帧、B帧用于表示传输的视频画面。 + +### I、P、B帧 + +在H.264协议中定义了3中帧,完整编码的帧叫I帧、参考之前的I帧生成的只对差异部分进行编码的帧叫P帧、还有一种参考前后的帧进行编码的帧叫B帧。 - I帧(Intra coded picture):帧内编码图像帧简称关键帧,这一帧画面的完整保留,解码时只需要本帧数据就可以完成.I帧通常是每个GOP(MPEG所使用的一种视频压缩技术)的第一个,GOP就是指两个I帧之间的距离。 我们在做直播的时候,想要能达到秒开的效果,因为I帧是能不依赖其他帧独立完成解码的,这就意味着当播放器接收到I帧就能立马渲染出来,而接收到P、B帧则需要等待依赖的帧而不能立马完成解码和渲染,这个期间就会黑屏,所以可以在服务端通过缓存GOP,保证播放端在接入直播时能先获取到I帧马上渲染出画面,从而提高起播速度。在直播服务器中,支持设置一个cache,用于存放GOP,直播服务器缓存了当前的GOP序列,当播放端请求数据的时候,CDN会从I帧返回给客户端,从而保证客户端可以快速进行播放。当然由于缓存的是之前的视频信息,当音频数据到达播放端后,为了音视频同步播放器会进行视频的快进处理。 - P帧(Predictive coded picture):预测编码图像帧简称差别帧,这一帧跟之前的一个关键帧或P帧的差别,解码时需要用之前缓存的画面叠加上本帧定义的差别,生成最终的画面。也就是说P帧没有完整的画面数据,只有与前一帧的画面差别的数据。 + - B帧(Bidirectionally predicted picture):双向预测编码图像帧简称双向差别帧,也就是B帧记录的是本帧与前后帧的差别。也就是要解码B帧,不仅要取得之前的缓存画面,还要解码之后的画面,通过前后画面与本帧数据的叠加来获取最终的画面。B帧压缩率高,但是解码时会更耗CPU。 -- GOP(Group of Pictures)是一组连续的画面,由一张I帧和数张B/P帧组成,是视频图像编码器和解码器存取的基本单位,它的排列顺序将会一直重复到影像结束。 - 编码器将多张图像进行编码后生产成一段一段的 GOP ,解码器在播放时则是读取一段一段的GOP进行解码后读取画面再渲染显示。 + + + +### GOP + +这3中帧用于表示传输的视频画面。在H.264中图像以序列为单位进行组织,一个序列是一段图像编码后的数据流,以I帧开始,到下一个I帧结束,中间部分也被称为一个GOP。 + +GOP(Group of Pictures)是一组连续的画面,由一张I帧和数张B/P帧组成,是视频图像编码器和解码器存取的基本单位,它的排列顺序将会一直重复到影像结束。 +编码器将多张图像进行编码后生产成一段一段的 GOP ,解码器在播放时则是读取一段一段的GOP进行解码后读取画面再渲染显示。 + +**编码器将多张图像进行编码后生产成一段一段的 GOP ( Group of Pictures ) , 解码器在播放时则是读取一段一段的 GOP 进行解码后读取画面再渲染显示。**GOP ( Group of Pictures) 是一组连续的画面,由一张 I 帧和数张 B / P 帧组成,是视频图像编码器和解码器存取的基本单位,它的排列顺序将会一直重复到影像结束。I 帧是内部编码帧(也称为关键帧),P帧是前向预测帧(前向参考帧),B 帧是双向内插帧(双向参考帧)。简单地讲,I 帧是一个完整的画面,而 P 帧和 B 帧记录的是相对于 I 帧的变化。如果没有 I 帧,P 帧和 B 帧就无法解码。 + +### IDR + +一个序列(GOP)的第一个图像叫做IDR图像(立即刷新图像),IDR图像都是I帧图像。H.264引入IDR图像是为了解码的重新同步,当解码器解码到IDR图像时,立即将参考帧队列清空,将已解码的数据全部输出或抛弃,重新查找下一个参考集,开始解码一个新的序列。这样,如果前一个序列出现重大错误,在这里可以获得重新同步的机会。IDR图像之后的图像永远不会使用IDR之前的图像数据来解码。一个序列就是一段内容差异不太大的图像编码后生成的一串数据流。当运动变化比较少时,一个序列可以很长,因为运动变化少就代表图像画面的内容变动很小,所以就可以是一个I帧,然后一直是P帧、B帧。当运动变化多时,一个序列可能会比较短,比如只包含一个I帧和几个P帧、B帧。 - IDR(Instantaneous Decoding Refresh)--即时解码刷新。 I帧:帧内编码帧是一种自带全部信息的独立帧,无需参考其它图像便可独立进行解码,视频序列中的第一个帧始终都是I帧。 + + 那么IDR帧与I帧的区别是什 么呢?因为H264采用了多帧预测,所以I帧之后的P帧有可能会参考I 帧之前的帧,这就使得在随机访问的时候不能以找到I帧作为参考条 件,因为即使找到I帧,I帧之后的帧还是有可能解析不出来,而IDR帧 就是一种特殊的I帧,即这一帧之后的所有参考帧只会参考到这个IDR 帧,而不会再参考前面的帧。在解码器中,一旦收到一个IDR帧,就会 立即清理参考帧缓冲区,并将IDR帧作为被参考的帧。 + + + I和IDR帧都是使用帧内预测的。它们都是同一个东西而已,在编码和解码中为了方便,要首个I帧和其他I帧区别开,所以才把第一个首个I帧叫IDR,这样就方便控制编码和解码流程。 IDR帧的作用是立刻刷新,使错误不致传播,从IDR帧开始,重新算一个新的序列开始编码。而I帧不具有随机访问的能力,这个功能是由IDR承担。 IDR会导致DPB(DecodedPictureBuffer 参考帧列表——这是关键所在)清空,而I不会。IDR图像一定是I图像,但I图像不一定是IDR图像。一个序列中可以有很多的I图像,I图像之后的图像可以引用I图像之间的图像做运动参考。一个序列中可以有很多的I图像,I图像之后的图象可以引用I图像之间的图像做运动参考。 对于IDR帧来说,在IDR帧之后的所有帧都不能引用任何IDR帧之前的帧的内容,与此相反,对于普通的I-帧来说,位于其之后的B-和P-帧可以引用位于普通I-帧之前的I-帧。从随机存取的视频流中,播放器永远可以从一个IDR帧播放,因为在它之后没有任何帧引用之前的帧。但是,不能在一个没有IDR帧的视频中从任意点开始播放,因为后面的帧总是会引用前面的帧 。 收到 IDR 帧时,解码器另外需要做的工作就是:把所有的 PPS 和 SPS 参数进行更新。 @@ -36,11 +57,11 @@ P帧和B帧极大的节省了数据量,节省出来的空间可以用来多保 - +### PTS和DTS 【为什么会有PTS和DTS的概念】 -通过上面的描述可以看出:P帧需要参考前面的I帧或P帧才可以生成一张完整的图片,而B帧则需要参考前面I帧或P帧及其后面的一个P帧才可以生成一张完整的图片。这样就带来了一个问题:在视频流中,先到来的 B 帧无法立即解码,需要等待它依赖的后面的 I、P 帧先解码完成,这样一来播放时间与解码时间不一致了,顺序打乱了,那这些帧该如何播放呢?这时就引入了另外两个概念:DTS 和 PTS。 +通过上面的描述可以看出:P帧需要参考前面的I帧或P帧才可以生成一张完整的图片,而B帧则需要参考前面I帧或P帧及其后面的一个P帧才可以生成一张完整的图片。这样就带来了一个问题:在视频流中,先到来的 B 帧无法立即解码,需要等待它依赖的后面的 I、P 帧先解码完成,这样一来播放时间与解码时间不一致了,顺序打乱了,那这些帧该如何播放呢?这时就引入了另外两个概念:DTS 和 PTS。在没有B帧的情况下,DTS和PTS的输出顺序是一样的。因为B 帧打乱了解码和显示的顺序,所以一旦存在B帧,PTS与DTS势必就会 不同。 【PTS和DTS】 diff --git "a/VideoDevelopment/\346\222\255\346\224\276\345\231\250\346\200\247\350\203\275\344\274\230\345\214\226.md" "b/VideoDevelopment/\346\222\255\346\224\276\345\231\250\346\200\247\350\203\275\344\274\230\345\214\226.md" index d1c96ecb..aebda409 100644 --- "a/VideoDevelopment/\346\222\255\346\224\276\345\231\250\346\200\247\350\203\275\344\274\230\345\214\226.md" +++ "b/VideoDevelopment/\346\222\255\346\224\276\345\231\250\346\200\247\350\203\275\344\274\230\345\214\226.md" @@ -66,6 +66,20 @@ 空间换时间。双播放器方案原理比较简单,就是在当前视频起播之后,预准备下一个视频的播放器,两个播放器循环使用。下一个视频的播放器一旦准备好,加上视频流已加载到本地,则起播非常快,几乎是视频直出的效果。但由于播放器准备更耗时,用户滑到下一个视频时,并不能百分百保证此视频的播放器已准备好。 +### 预取策略 + +1,修改AndroidVideoCache进行预加载 + 2,线程池并发缓存并控制视频缓存优先级(线程池线程数为3,先加入的先缓存),一次预加载8个视频,item创建时开始预加载,item销毁时,取消预加载 + 3,等待下页第一个视频预加载完成,才会进入下一页视频,保证滑到的视频都是可以立马观看的。(一页视频为8个) + 4,当前视频开始播放之后才会进行预加载 + 5,区分快滑慢滑两种模式,快滑时取消当前所有预加载,慢滑不取消,因为快滑大概率滑到一个未预加载的视频,慢滑大概率滑到一个已经预加载的视频,保证滑到视频尽快播放。 + 6,当视频播放后,根据滑动方向,会将取消的预加载进行恢复,正向滑动就恢复当前之后之后的预加载任务,反选滑动就恢复当前视频之前的预加载任务。 + 7,限制网速,当时视频还没播放的时候,会限制预下载的网速(慢滑不会取消预加载),通过让下载线程sleep来限制网速。 + 8,ijkplayer起播参数配置。 + 9,视频压缩和处理(让视频参数都位于视频首部)。 + + + #### MOOV后置 很多手机为了偷懒,录制完视频后才知道视频信息,所以把moov放到末尾。对于moov放到视频最后的情况下,就要多一次seek,当从头开始解析的时候如果发现未找到moov信息,需要将其seek到尾部再去寻找,这样会导致起播慢。 diff --git "a/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/TS.md" "b/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/TS.md" index c0a2daad..947ab46a 100644 --- "a/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/TS.md" +++ "b/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/TS.md" @@ -3,23 +3,125 @@ TS -TODO +TS的全称为MPEG2-TS,TS即Transport Stream的缩写。它是分包发送的,每一个包长188字节(还有192和204字节的包)。包的结构为,包头4字节(第一个字节为0x47),负载为184字节。 +VD的音视频格式为MPEG2-PS,全称是Program Stream。而TS的全称则是Transport Stream。MPEG2-PS主要应用于存储的具有固定时长的节目,如DVD电影,而MPEG-TS则主要应用于实时传送的节目,比如实时广播的电视节目。这两种格式的主要区别是什么呢?简单地打个比喻说,你将DVD上的VOB文件的前面一截cut掉(或者干脆就是数据损坏),那么就会导致整个文件无法解码了,而电视节目是你任何时候打开电视机都能解码(收看)的。在TS流里可以填入很多类型的数据,如视频、音频、自定义信息等。所以,MPEG2-TS格式的特点就是要求从视频流的任一片段开始都是可以独立解码的。 +我们可以看出,TS格式是主要用于直播的码流结构,具有很好的容错能力。通常TS流的后缀是.ts、.mpg或者.mpeg,多数播放器直接支持这种格式的播放。TS流中不包含快速seek的机制,只能通过协议层实现seek。HLS协议基于TS流实现的。 +## TS格式详解 +TS文件(流)可以分为三层:TS层(Transport Stream)、PES层(Packet Elemental Stream)、ES层(Elementary Stream)。 +ES层就是音视频数据,PES层是在音视频数据上加了时间戳等对数据帧的说明信息,TS层是在PES层上加入了数据流识别和传输的必要信息。TS文件(码流)由多个TS Packet组成的。 +![image-20210309114928103](https://raw.githubusercontent.com/CharonChui/Pictures/master/ts_archi.png?raw=true) +### TS层 +TS包大小固定为188字节,TS层分为三个部分:TS Header、Adaptation Field、Payload。 +TS Header固定4个字节;Adaptation Field可能存在也可能不存在,主要作用是给不足188字节的数据做填充;Payload是PES数据。 +#### 1. TS Header + +TS包的包头提供关于传输方面的信息。 + +TS包的包头长度不固定,前4个字节是固定的,后面可能跟有自适应字段(适配域)。4个字节是最小包头。 + +包头的结构体字段如下: + +- sync_byte(同步字节):固定为0x47;该字节由解码器识别,使包头和有效负载可相互分离。 +- transport_error_indicator(传输错误标志):‘1’表示在相关的传输包中至少有一个不可纠正的错误位。当被置1后,在错误被纠正之前不能重置为0。 +- payload_unit_start_indicator(负载起始标志):为1时,表示当前TS包的有效载荷中包含PES或者PSI的起始位置;在前4个字节之后会有一个调整字节,其的数值为后面调整字段的长度length。因此有效载荷开始的位置应再偏移1+[length]个字节。 +- transport_priority(传输优先级标志):‘1’表明当前TS包的优先级比其他具有相同PID, 但此位没有被置‘1’的TS包高。 +- PID:指示存储与分组有效负载中数据的类型。 +- transport_scrambling_control(加扰控制标志):表示TS流分组有效负载的加密模式。空包为‘00’,如果传输包包头中包括调整字段,不应被加密。其他取值含义是用户自定义的。 +- adaptation_field_control(适配域控制标志):表示包头是否有调整字段或有效负载。‘00’为ISO/IEC未来使用保留;‘01’仅含有效载荷,无调整字段;‘10’ 无有效载荷,仅含调整字段;‘11’ 调整字段后为有效载荷,调整字段中的前一个字节表示调整字段的长度length,有效载荷开始的位置应再偏移[length]个字节。空包应为‘10’。 +- continuity_counter(连续性计数器):随着每一个具有相同PID的TS流分组而增加,当它达到最大值后又回复到0。范围为0~15。 + +#### 2. TS Adaptation Field + +Adaptation Field的长度要包含传输错误指示符标识的一个字节。 + +PCR是节目时钟参考,PCR、DTS、PTS都是对同一个系统时钟的采样值,PCR是递增的,因此可以将其设置为DTS值,音频数据不需要PCR。 + +打包TS流时PAT和PMT表是没有Adaptation Field的,不够的长度直接补0xff即可。 + +视频流和音频流都需要加adaptation field,通常加在一个帧的第一个ts包和最后一个ts包里,中间的ts包不加。 + +#### 3. TS Payload + +TS包中Payload所传输的信息包括两种类型:视频、音频的PES包以及辅助数据;节目专用信息PSI。 + +TS包也可以是空包。空包用来填充TS流,可能在重新进行多路复用时被插入或删除。 + +视频、音频的ES流需进行打包形成视频、音频的 PES流。辅助数据(如图文电视信息)不需要打成PES包。 + +### PES层 & ES 层 + +#### PES层 + +PES结构如图: + +![pes](https://raw.githubusercontent.com/CharonChui/Pictures/master/pes.png?raw=true) + +从上面的结构图可以看出,PES层是在每一个视频/音频帧上加入了时间戳等信息,PES包内容很多,下面我们说明一下最常用的字段: + +- pes start code:开始码,固定为0x000001。 +- stream id:音频取值(0xc0-0xdf),通常为0xc0;视频取值(0xe0-0xef),通常为0xe0。 +- pes packet length:后面pes数据的长度,0表示长度不限制,只有视频数据长度会超过0xffff。 +- pes data length:后面数据的长度,取值5或10。 +- pts:33bit值 +- dts:33bit值 + +关于时间戳PTS和DTS的说明: + +1. PTS是显示时间戳、DTS是解码时间戳。 +2. 视频数据两种时间戳都需要,音频数据的PTS和DTS相同,所以只需要PTS。 + +有PTS和DTS两种时间戳是B帧引起的,I帧和P帧的PTS等于DTS。如果一个视频没有B帧,则PTS永远和DTS相同。 + +从文件中顺序读取视频帧,取出的帧顺序和DTS顺序相同。DTS算法比较简单,初始值 + 增量即可,PTS计算比较复杂,需要在DTS的基础上加偏移量。 + +音频的PES中只有PTS(同DTS),视频的I、P帧两种时间戳都要有,视频B帧只要PTS(同DTS)。 + +#### ES 层 + +ES层指的就是音视频数据。 + +一般的,视频为H.264视频,音频为AAC音频。 + + + + + +## TS流生成及解析流程 + +### TS 流生成流程 + +- 将原始音视频数据压缩之后,压缩结果组成一个基本码流(ES)。 +- 对ES(基本码流)进行打包形成PES。 +- 在PES包中加入时间戳信息(PTS/DTS)。 +- 将PES包内容分配到一系列固定长度的传输包(TS Packet)中。 +- 在传输包中加入定时信息(PCR)。 +- 在传输包中加入节目专用信息(PSI) 。 +- 连续输出传输包形成具有恒定比特率的MPEG-TS流。 + +### TS 流解析流程 + +- 复用的MPEG-TS流中解析出TS包; +- 从TS包中获取PAT及对应的PMT; +- 从而获取特定节目的音视频PID; +- 通过PID筛选出特定音视频相关的TS包,并解析出PES; +- 从PES中读取到PTS/DTS,并从PES中解析出基本码流ES; +- 将ES交给解码器,获得压缩前的原始音视频数据。 diff --git "a/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217.md" "b/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217.md" index 8899f1a8..a81d1123 100644 --- "a/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217.md" +++ "b/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217.md" @@ -8,6 +8,14 @@ +封装,也叫多路复用(mux)。封装的目的一般为了在一个文件(流)中能同时存储视频(video)、音频(audio)、字幕(subtitle)等内容——这也正是“复用”的含义所在(分时复用)。封装还有另一个作用是在网络环境下确保数据的可靠快速传输。 + +编码的目的是为了压缩媒体数据。有别于通用文件数据的压缩,在图像或音频压缩的时候,可以借助图像特性(如前后关联、相邻图块关联)或声音特性(听觉模型)进行压缩,可以达到比通用压缩技术更高的压缩比。 + + + + + ## 音频编码格式 From c9f76c0a10c3270e2d36c8d9d1feee299bd7b326 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 16 Mar 2021 21:07:24 +0800 Subject: [PATCH 046/213] add notes --- ...01\350\231\232\345\274\225\347\224\250.md" | 15 +- ...20\347\240\201\345\210\206\346\236\220.md" | 792 ++++++++++++++++++ ...20\347\240\201\350\257\246\350\247\243.md" | 6 +- .../\345\205\263\351\224\256\345\270\247.md" | 2 +- 4 files changed, 804 insertions(+), 11 deletions(-) create mode 100644 "SourceAnalysis/LeakCanary\346\272\220\347\240\201\345\210\206\346\236\220.md" diff --git "a/JavaKnowledge/\345\274\272\345\274\225\347\224\250\343\200\201\350\275\257\345\274\225\347\224\250\343\200\201\345\274\261\345\274\225\347\224\250\343\200\201\350\231\232\345\274\225\347\224\250.md" "b/JavaKnowledge/\345\274\272\345\274\225\347\224\250\343\200\201\350\275\257\345\274\225\347\224\250\343\200\201\345\274\261\345\274\225\347\224\250\343\200\201\350\231\232\345\274\225\347\224\250.md" index 1f4346c1..e5dba493 100644 --- "a/JavaKnowledge/\345\274\272\345\274\225\347\224\250\343\200\201\350\275\257\345\274\225\347\224\250\343\200\201\345\274\261\345\274\225\347\224\250\343\200\201\350\231\232\345\274\225\347\224\250.md" +++ "b/JavaKnowledge/\345\274\272\345\274\225\347\224\250\343\200\201\350\275\257\345\274\225\347\224\250\343\200\201\345\274\261\345\274\225\347\224\250\343\200\201\350\231\232\345\274\225\347\224\250.md" @@ -16,21 +16,22 @@ - 强引用(Strong Reference) - + 你懂的,不要胡乱持有着不放,不然内存泄露、oom有你好看,就像是老板(OOM)的亲儿子一样,在公司可以什么事都不干,但是千万不要老是占用公司的资源为他自己做事,记得用完公司的妹子之后,要让她们去工作(资源要懂得释放) 不然公司很可能会垮掉的。 平时我们编程的时候例如:`Object object=new Object()`;那`object`就是一个强引用了。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当JVM的内存空间不足时,宁愿抛出OutOfMemoryError使得程序异常终止也不愿意回收具有强引用的存活着的对象!记住是存活着,不可能是你new一个对象就永远不会被GC回收。 - + - 软引用(SoftReference) - + 描述一些还有用,但并非必需的对象。如果一个对象只具有软引用,那就类似于可有可物的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存,但是system.gc对其无效,有点像老板(OOM)的亲戚,在公司表现不好有可能会被开除,即使你投诉他(调用GC)上班看片,但是只要不被老板看到(被JVM检测到)就不会被开除(被虚拟机回收)。 + **软引用可用来实现内存敏感的高速缓存**。 软引用可以和一个引用队列`(ReferenceQueue)`联合使用, 如果软引用所引用的对象被垃圾回收,`Java`虚拟机就会把这个软引用加入到与之关联的引用队列中。 - 弱引用(WeakReference) - - 同软引用,也用来描述非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。在对象没有其他引用的情况下,调用system.gc对象可被虚拟机回收,就是一个普通的员工,平常如果表现不佳会被开除(对象没有其他引用的情况下),遇到别人投诉(调用GC)上班看片,那开除是肯定了(被虚拟机回收)。 + + 同软引用,也用来描述非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。所以弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的声明周期。在对象没有其他引用的情况下,调用system.gc对象可被虚拟机回收,就是一个普通的员工,平常如果表现不佳会被开除(对象没有其他引用的情况下),遇到别人投诉(调用GC)上班看片,那开除是肯定了(被虚拟机回收)。 在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了***只具***有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。弱引用最常见的用途是实现规范映射(canonicalizing mappings,比如哈希表)。 @@ -60,12 +61,12 @@ ``` - 虚引用(PhantomReference) - + "虚引用"顾名思义,就是形同虚设,也成为幽灵引用或幻影引用,它是最弱的一种引用关系。就只是一个标识,对象的生命周期不受期影响,这货估计就是个临时工把,遇到事情的时候想到了你,没有事情的时候,秒秒钟拿出去顶锅,开除。 一个对象是否有虚引用的存在完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一的用处:能在对象被GC时收到系统通知,主要用于跟踪对象何时被回收,比如防止资源泄漏等。 - 虚引用必须和引用队列 `(ReferenceQueue)`联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。 虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null。 + 虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。 虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null。 diff --git "a/SourceAnalysis/LeakCanary\346\272\220\347\240\201\345\210\206\346\236\220.md" "b/SourceAnalysis/LeakCanary\346\272\220\347\240\201\345\210\206\346\236\220.md" new file mode 100644 index 00000000..62669286 --- /dev/null +++ "b/SourceAnalysis/LeakCanary\346\272\220\347\240\201\345\210\206\346\236\220.md" @@ -0,0 +1,792 @@ +LeakCanary源码分析 +=== + +[LeakCanary](https://github.com/square/leakcanary)是一个用于检测内存泄漏的工具,可以用于Java和Android,是由著名开源组织Square贡献。 + +强烈建议使用LeakCanary 2.x版本,更高效、使用更简单,而且没有任何Java代码,它当泄露引用到达5时才会发起heap dump,同时使用了全新的heap parser,减少内存占用,提升速度。只需要在dependencies中加入leakcanary的依赖即可。而且debugimplementation只在debug模式下有效,所以不用担心用户在正式环境下也会出现LeanCanary收集。而且是完全使用kotlin实现了,同时使用了[see Shark](https://square.github.io/leakcanary/shark/)来进行heap内存分析,更节省内存。 + + + +### 使用 + +只需在app的build.gradle中按如下配置即可,没有任何其他代码: + +```xml +dependencies { + // debugImplementation because LeakCanary should only run in debug builds. + debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.6' +} +``` + + + +到这里你肯定会有个疑问:一行Java代码都没有,它是如何做到监控的? 后面我们就带着这个疑问来进行源码查看。不过再看源码之前,我们需要先了解一些引用的知识[**强引用、软引用、弱引用、虚引用**](https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/%E5%BC%BA%E5%BC%95%E7%94%A8%E3%80%81%E8%BD%AF%E5%BC%95%E7%94%A8%E3%80%81%E5%BC%B1%E5%BC%95%E7%94%A8%E3%80%81%E8%99%9A%E5%BC%95%E7%94%A8.md) + +这篇文章中在介绍弱引用时是这样说的: + +同软引用,也用来描述非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。在对象没有其他引用的情况下,调用system.gc对象可被虚拟机回收,就是一个普通的员工,平常如果表现不佳会被开除(对象没有其他引用的情况下),遇到别人投诉(调用GC)上班看片,那开除是肯定了(被虚拟机回收)。 + +在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了***只具***有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。 + +而LeakCanary的原因也正是使用了弱引用的这个特点。 + +### 监测原理 + +ReferenceQueue+WeakReference+手动调用GC可实现这个需求。 + +WeakReference和ReferenceQueue联合使用, +当被WeakReference引用的对象的生命周期结束,一旦被GC检查到,GC将会把该对象添加到ReferenceQueue中,待ReferenceQueue处理。 +当GC过后对象一直不被加入ReferenceQueue,它可能存在内存泄漏。 + +LeakCanary的基础是一个叫做ObjectWatcher Android的library。它hook了Android的生命周期,当activity和fragment 被销毁并且应该被垃圾回收时候自动检测。这些被销毁的对象被传递给ObjectWatcher, ObjectWatcher持有这些被销毁对象的弱引用(weak references)。如果弱引用在等待5秒钟并运行垃圾收集器后仍未被清除,那么被观察的对象就被认为是保留的(retained,在生命周期结束后仍然保留),并存在潜在的泄漏。LeakCanary会在Logcat中输出这些日志。 + +### 监测内容 + +LeakCanary会自动监测如下对象的泄露情况: + +- destroyed `Activity` instances +- destroyed `Fragment` instances +- destroyed fragment `View` instances +- cleared `ViewModel` instances + + + +### 监测步骤 + +一旦LeakCanary被安装,它会自动监测和上报泄露,分为以下四步: + +1. Detecting retained objects. +2. Dumping the heap. +3. Analyzing the heap. +4. Categorizing leaks. + + + +好了,我们接下来从源码的角度来看一下具体的实现,但是看到代码我懵逼了,从哪里看呢? 我完全没头绪。 + +![leakcanary_source](https://raw.githubusercontent.com/CharonChui/Pictures/master/leakcanary_source.png) + +那就继续看官方介绍吧。 + +## 1. Detecting retained objects[¶](https://square.github.io/leakcanary/fundamentals-how-leakcanary-works/#1-detecting-retained-objects) + +LeakCanary hooks into the Android lifecycle to automatically detect when activities and fragments are destroyed and should be garbage collected. These destroyed objects are passed to an `ObjectWatcher`, which holds [weak references](https://en.wikipedia.org/wiki/Weak_reference) to them. LeakCanary automatically detects leaks for the following objects: + +- destroyed `Activity` instances +- destroyed `Fragment` instances +- destroyed fragment `View` instances +- cleared `ViewModel` instances + +You can watch any objects that is no longer needed, for example a detached view or a destroyed presenter: + +``` +AppWatcher.objectWatcher.watch(myDetachedView, "View was detached") +``` + +If the weak reference held by `ObjectWatcher` isn’t cleared after **waiting 5 seconds** and running garbage collection, the watched object is considered **retained**, and potentially leaking. LeakCanary logs this to Logcat: + +``` +D LeakCanary: Watching instance of com.example.leakcanary.MainActivity + (Activity received Activity#onDestroy() callback) + +... 5 seconds later ... + +D LeakCanary: Scheduling check for retained objects because found new object + retained +``` + +LeakCanary waits for the count of retained objects to reach a threshold before dumping the heap, and displays a notification with the latest count. + + + + + +## 2. Dumping the heap[¶](https://square.github.io/leakcanary/fundamentals-how-leakcanary-works/#2-dumping-the-heap) + +When the count of retained objects reaches a threshold, LeakCanary dumps the Java heap into a `.hprof` file (a **heap dump**) stored onto the Android file system (see [Where does LeakCanary store heap dumps?](https://square.github.io/leakcanary/faq/#where-does-leakcanary-store-heap-dumps)). Dumping the heap freezes the app for a short amount of time, during which LeakCanary displays the following toast: + + + +## 3. Analyzing the heap[¶](https://square.github.io/leakcanary/fundamentals-how-leakcanary-works/#3-analyzing-the-heap) + +LeakCanary parses the `.hprof` file using [Shark](https://square.github.io/leakcanary/shark/) and locates the retained objects in that heap dump. + + + +## How does LeakCanary get installed by only adding a dependency?[¶](https://square.github.io/leakcanary/faq/#how-does-leakcanary-get-installed-by-only-adding-a-dependency) + +On Android, content providers are created after the Application instance is created but before Application.onCreate() is called. The `leakcanary-object-watcher-android` artifact has a non exported ContentProvider defined in its `AndroidManifest.xml` file. When that ContentProvider is installed, it adds activity and fragment lifecycle listeners to the application. + +对于Application的onCreate()方法,官方文档中是这样说的: + +Called when the application is starting, before any activity, service, or receiver objects(excluding content providers)have been created. + + + +看到这里,大呼内行...这个实现太巧妙了。尽然是通过在AndroidManifest.xml中注册一个内容提供者,这个内容提供者的创建会在Application.onCreate()之前,在它创建的时候去做install的操作。 + + + +这也太牛逼了,之前从来没想到ContentProvider竟然还有这个功能,以后估计很多第三方的库都会这样来实现,但是这样也会有一个问题,就是会影响到应用的冷启时间,好在LeakCanary只是在Debug的时候使用,也就不会存在这个问题了。 + +好了,可以看代码了,先看一下这些module的依赖关系,Gradle中的module下Task中的help中的dependencies: + +``` +debugRuntimeClasspath - Runtime classpath of compilation 'debug' (target (androidJvm)). ++--- project :leakcanary-android +| +--- project :leakcanary-android-core +| | +--- project :shark-android +| | | +--- project :shark +| | | | +--- project :shark-graph +| | | | | +--- project :shark-hprof +| | | | | | +--- project :shark-log +| | | | | | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.21 +| | | | | | | +--- org.jetbrains.kotlin:kotlin-stdlib-common:1.4.21 +| | | | | | | \--- org.jetbrains:annotations:13.0 +| | | | | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.21 (*) +| | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.21 (*) +| | | | | \--- com.squareup.okio:okio:2.2.2 +| | | | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.2.60 -> 1.4.21 (*) +| | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.21 (*) +| | | | \--- com.squareup.okio:okio:2.2.2 (*) +| | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.21 (*) +| | +--- project :leakcanary-object-watcher-android +| | | +--- project :leakcanary-object-watcher +| | | | +--- project :shark-log (*) +| | | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.21 (*) +| | | +--- project :leakcanary-android-utils +| | | | +--- project :shark-log (*) +| | | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.21 (*) +| | | +--- com.squareup.curtains:curtains:1.0.1 +| | | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.4.21 (*) +| | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.21 (*) +| | +--- project :leakcanary-object-watcher-android-androidx +| | | +--- project :leakcanary-object-watcher-android (*) +| | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.21 (*) +| | +--- project :leakcanary-object-watcher-android-support-fragments +| | | +--- project :leakcanary-object-watcher-android (*) +| | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.21 (*) +| | +--- project :plumber-android +| | | +--- project :shark-log (*) +| | | +--- project :leakcanary-android-utils (*) +| | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.21 (*) +| | \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.21 (*) +| \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.21 (*) +\--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.21 (*) + +``` + + + +那我们就去`leakcanary-object-watcher-android`库的AndroidManifest.xml中看一下这个内容提供者: + +```xml + + + +``` + +继续看一下AppWatcherInstaller类的实现: + +```kotlin +/** + * Content providers are loaded before the application class is created. [AppWatcherInstaller] is + * used to install [leakcanary.AppWatcher] on application start. + */ +internal sealed class AppWatcherInstaller : ContentProvider() { + + /** + * [MainProcess] automatically sets up the LeakCanary code that runs in the main app process. + */ + internal class MainProcess : AppWatcherInstaller() + + /** + * When using the `leakcanary-android-process` artifact instead of `leakcanary-android`, + * [LeakCanaryProcess] automatically sets up the LeakCanary code + */ + internal class LeakCanaryProcess : AppWatcherInstaller() + + override fun onCreate(): Boolean { + val application = context!!.applicationContext as Application + AppWatcher.manualInstall(application) + return true + } + + override fun query( + uri: Uri, + strings: Array?, + s: String?, + strings1: Array?, + s1: String? + ): Cursor? { + return null + } + + override fun getType(uri: Uri): String? { + return null + } + + override fun insert( + uri: Uri, + contentValues: ContentValues? + ): Uri? { + return null + } + + override fun delete( + uri: Uri, + s: String?, + strings: Array? + ): Int { + return 0 + } + + override fun update( + uri: Uri, + contentValues: ContentValues?, + s: String?, + strings: Array? + ): Int { + return 0 + } +} +``` + +上面的注释说的很明白,这个内容提供者的主要目的就是调用AppWatcher.install,我们继续看一下AppWatcher.manualInstall()方法: + +```kot + @JvmOverloads + fun manualInstall( + application: Application, + retainedDelayMillis: Long = TimeUnit.SECONDS.toMillis(5), + watchersToInstall: List = appDefaultWatchers(application) + ) { + // 检查是否是主线程 + checkMainThread() + // 检查是否已经installed + check(!isInstalled) { + "AppWatcher already installed" + } + check(retainedDelayMillis >= 0) { + "retainedDelayMillis $retainedDelayMillis must be at least 0 ms" + } + this.retainedDelayMillis = retainedDelayMillis + if (application.isDebuggableBuild) { + // debug模式的话就初始化泄露信息的logcat打印 + LogcatSharkLog.install() + } + // Requires AppWatcher.objectWatcher to be set + LeakCanaryDelegate.loadLeakCanary(application) + + watchersToInstall.forEach { + // 不传递的话,默认为appDefaultWatchers,将这里面的四个类型的检测器各自初始化 + it.install() + } + } + + // appDefaultWatchers()的实现为: + fun appDefaultWatchers( + application: Application, + reachabilityWatcher: ReachabilityWatcher = objectWatcher + ): List { + return listOf( + ActivityWatcher(application, reachabilityWatcher), + FragmentAndViewModelWatcher(application, reachabilityWatcher), + RootViewWatcher(reachabilityWatcher), + ServiceWatcher(reachabilityWatcher) + ) + } + +``` + +这里我们以ActivityWatcher为例,看一下它的install实现: + +```kotlin +/** + * Expects activities to become weakly reachable soon after they receive the [Activity.onDestroy] + * callback. + */ +class ActivityWatcher( + private val application: Application, + private val reachabilityWatcher: ReachabilityWatcher +) : InstallableWatcher { + + private val lifecycleCallbacks = + object : Application.ActivityLifecycleCallbacks by noOpDelegate() { + override fun onActivityDestroyed(activity: Activity) { + reachabilityWatcher.expectWeaklyReachable( + activity, "${activity::class.java.name} received Activity#onDestroy() callback" + ) + } + } + + override fun install() { + application.registerActivityLifecycleCallbacks(lifecycleCallbacks) + } + + override fun uninstall() { + application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks) + } +} +``` + +这里面的实现也很简单,就是通过Application.registerActivityLifecycleCallbacks然后在activity destroy的回调中去执行ReachabilityWatcher接口的expectWeaklyReachable,ReachabilityWatcher接口的实现类是ObjectWatcher类: + +```kotlin +class ObjectWatcher constructor( + private val clock: Clock, + private val checkRetainedExecutor: Executor, + /** + * Calls to [watch] will be ignored when [isEnabled] returns false + */ + private val isEnabled: () -> Boolean = { true } +) : ReachabilityWatcher { + // ... + // Map集合,用来存放要观察对象的key和弱引用,代码会为每个观察的对象生成一个唯一的key和弱应用 + private val watchedObjects = mutableMapOf() + // 这个队列是与弱引用联合使用,当弱引用中的对象被回收后,这个弱引用会被放到这个队列中。 + // 也就是说只要存在与这个队列中的弱引用就代表该弱引用所包含的对象被回收了。 + private val queue = ReferenceQueue() + + @Synchronized override fun expectWeaklyReachable( + watchedObject: Any, + description: String + ) { + if (!isEnabled()) { + return + } + // 清理操作:先移除watchedObjects和queue中已经被回收的对象的弱引用 + removeWeaklyReachableObjects() + // 生成唯一的key + val key = UUID.randomUUID() + .toString() + val watchUptimeMillis = clock.uptimeMillis() + // 用要监听的对象创建KeyedWeakReference + val reference = + KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue) + SharkLog.d { + "Watching " + + (if (watchedObject is Class<*>) watchedObject.toString() else "instance of ${watchedObject.javaClass.name}") + + (if (description.isNotEmpty()) " ($description)" else "") + + " with key $key" + } + + watchedObjects[key] = reference + // 这是一个延迟任务,延迟五秒后再次执行moveToRetained()方法 + checkRetainedExecutor.execute { + moveToRetained(key) + } + } +} +``` + +我们继续看moveToRetained()方法的实现: + +```kotlin + @Synchronized private fun moveToRetained(key: String) { + // 该方法会调用removeWeaklyReachableObjects再去执行一次清理操作 + // 因为这个期间对象可能已经被回收了,所以需要再清理一次 + removeWeaklyReachableObjects() + val retainedRef = watchedObjects[key] + // 等再执行完清理操作后,那还在watchedObjects集合中的对象就是没有被回收的对象了 + if (retainedRef != null) { + retainedRef.retainedUptimeMillis = clock.uptimeMillis() + onObjectRetainedListeners.forEach { it.onObjectRetained() } + } + } +``` + +继续看OnObjectRetainedListener接口的onObjectRetained()方法,这里它的实现类是InternalLeakCanary: + +```kotlin + private lateinit var heapDumpTrigger: HeapDumpTrigger + + override fun onObjectRetained() = scheduleRetainedObjectCheck() + + fun scheduleRetainedObjectCheck() { + if (this::heapDumpTrigger.isInitialized) { + heapDumpTrigger.scheduleRetainedObjectCheck() + } + } +``` + +继续看HeapDumpTrigger类的scheduleRetainedObjectCheck(): + +```kotlin + fun scheduleRetainedObjectCheck( + delayMillis: Long = 0L + ) { + val checkCurrentlyScheduledAt = checkScheduledAt + if (checkCurrentlyScheduledAt > 0) { + return + } + checkScheduledAt = SystemClock.uptimeMillis() + delayMillis + backgroundHandler.postDelayed({ + checkScheduledAt = 0 + checkRetainedObjects() + }, delayMillis) + } +``` + +这里会立即执行checkRetainedObjects()方法: + +```kotlin +private fun checkRetainedObjects() { + // 调用ObjectWatcher中watchedObjects集合中对象retainedUptimeMillis != -1的数量 + // 这些是可能发生泄漏的对象数量 + var retainedReferenceCount = objectWatcher.retainedObjectCount + if (retainedReferenceCount > 0) { + // 如果数量> 0,调用gc + gcTrigger.runGc() + // 调用完gc后重新获取数量 + retainedReferenceCount = objectWatcher.retainedObjectCount + } + // 如果个数小于5,不做操作等待5秒再次进行检查未回收的个数,一直循环,直到大于等于5个或者等于0个,为了防止频发回收堆造成卡顿。 + // 大于5个后,如果处于debug模式,会再等20秒,再次执行循环gc的操作。防止debug模式会减慢回收 + if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return + + val now = SystemClock.uptimeMillis() + val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillis + // 距离上次堆栈分析是否大于等于1分钟,如果没有超过一分钟,也需要再次延迟(1分钟-当前距离上次的时间)再次循环gc的操作 + if (elapsedSinceLastDumpMillis < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) { + onRetainInstanceListener.onEvent(DumpHappenedRecently) + showRetainedCountNotification( + objectCount = retainedReferenceCount, + contentText = application.getString(R.string.leak_canary_notification_retained_dump_wait) + ) + scheduleRetainedObjectCheck( + delayMillis = WAIT_BETWEEN_HEAP_DUMPS_MILLIS - elapsedSinceLastDumpMillis + ) + return + } + + dismissRetainedCountNotification() + val visibility = if (applicationVisible) "visible" else "not visible" + // 最终调用dump + dumpHeap( + retainedReferenceCount = retainedReferenceCount, + retry = true, + reason = "$retainedReferenceCount retained objects, app is $visibility" + ) + } +``` + +继续看dumpHeap()方法: + +```kotlin + private fun dumpHeap( + retainedReferenceCount: Int, + retry: Boolean, + reason: String + ) { + saveResourceIdNamesToMemory() + val heapDumpUptimeMillis = SystemClock.uptimeMillis() + KeyedWeakReference.heapDumpUptimeMillis = heapDumpUptimeMillis + // HeapDumper.dumpHeap()方法来获取Heap信息,生成hprof文件 + when (val heapDumpResult = heapDumper.dumpHeap()) { + is NoHeapDump -> { + if (retry) { + SharkLog.d { "Failed to dump heap, will retry in $WAIT_AFTER_DUMP_FAILED_MILLIS ms" } + scheduleRetainedObjectCheck( + delayMillis = WAIT_AFTER_DUMP_FAILED_MILLIS + ) + } else { + SharkLog.d { "Failed to dump heap, will not automatically retry" } + } + showRetainedCountNotification( + objectCount = retainedReferenceCount, + contentText = application.getString( + R.string.leak_canary_notification_retained_dump_failed + ) + ) + } + is HeapDump -> { + lastDisplayedRetainedObjectCount = 0 + lastHeapDumpUptimeMillis = SystemClock.uptimeMillis() + // 清楚早于heapDumpUptimeMillis之前的元素 + objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis) + // 将heap信息文件交给HeapAnalyzerService进行分析 + HeapAnalyzerService.runAnalysis( + context = application, + heapDumpFile = heapDumpResult.file, + heapDumpDurationMillis = heapDumpResult.durationMillis, + heapDumpReason = reason + ) + } + } + } +``` + + + +所以我们要分两部分量看: + +##### 1. HeapDumper接口的实现类是AndroidHeapDumper.dumpHeap() + +```kotlin + override fun dumpHeap(): DumpHeapResult { + val heapDumpFile = leakDirectoryProvider.newHeapDumpFile() ?: return NoHeapDump + + val waitingForToast = FutureResult() + showToast(waitingForToast) + + if (!waitingForToast.wait(5, SECONDS)) { + SharkLog.d { "Did not dump heap, too much time waiting for Toast." } + return NoHeapDump + } + + val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + if (Notifications.canShowNotification) { + val dumpingHeap = context.getString(R.string.leak_canary_notification_dumping) + val builder = Notification.Builder(context) + .setContentTitle(dumpingHeap) + val notification = Notifications.buildNotification(context, builder, LEAKCANARY_LOW) + notificationManager.notify(R.id.leak_canary_notification_dumping_heap, notification) + } + + val toast = waitingForToast.get() + + return try { + val durationMillis = measureDurationMillis { + // 系统Debug.dumpHprofData方法进行堆转储,保存在yyyy-MM-dd_HH-mm-ss_SSS.hprof + Debug.dumpHprofData(heapDumpFile.absolutePath) + } + if (heapDumpFile.length() == 0L) { + SharkLog.d { "Dumped heap file is 0 byte length" } + NoHeapDump + } else { + HeapDump(file = heapDumpFile, durationMillis = durationMillis) + } + } catch (e: Exception) { + SharkLog.d(e) { "Could not dump heap" } + // Abort heap dump + NoHeapDump + } finally { + cancelToast(toast) + notificationManager.cancel(R.id.leak_canary_notification_dumping_heap) + } + } +``` + + + + + +##### 2. HeapAnalyzerService.runAnalysis() + +```kotlin +companion object { + private const val HEAPDUMP_FILE_EXTRA = "HEAPDUMP_FILE_EXTRA" + private const val HEAPDUMP_DURATION_MILLIS_EXTRA = "HEAPDUMP_DURATION_MILLIS_EXTRA" + private const val HEAPDUMP_REASON_EXTRA = "HEAPDUMP_REASON_EXTRA" + private const val PROGUARD_MAPPING_FILE_NAME = "leakCanaryObfuscationMapping.txt" + + fun runAnalysis( + context: Context, + heapDumpFile: File, + heapDumpDurationMillis: Long? = null, + heapDumpReason: String = "Unknown" + ) { + val intent = Intent(context, HeapAnalyzerService::class.java) + intent.putExtra(HEAPDUMP_FILE_EXTRA, heapDumpFile) + intent.putExtra(HEAPDUMP_REASON_EXTRA, heapDumpReason) + heapDumpDurationMillis?.let { + intent.putExtra(HEAPDUMP_DURATION_MILLIS_EXTRA, heapDumpDurationMillis) + } + startForegroundService(context, intent) + } + + private fun startForegroundService( + context: Context, + intent: Intent + ) { + if (SDK_INT >= 26) { + context.startForegroundService(intent) + } else { + // Pre-O behavior. + context.startService(intent) + } + } + } +``` + +这里面会去启动HeapAnalyzerService,然后会执行到onHandleIntentInForeground()中: + +```kotlin +override fun onHandleIntentInForeground(intent: Intent?) { + if (intent == null || !intent.hasExtra(HEAPDUMP_FILE_EXTRA)) { + SharkLog.d { "HeapAnalyzerService received a null or empty intent, ignoring." } + return + } + + // Since we're running in the main process we should be careful not to impact it. + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND) + // 取出传递过来的hprof文件信息 + val heapDumpFile = intent.getSerializableExtra(HEAPDUMP_FILE_EXTRA) as File + val heapDumpReason = intent.getStringExtra(HEAPDUMP_REASON_EXTRA) + val heapDumpDurationMillis = intent.getLongExtra(HEAPDUMP_DURATION_MILLIS_EXTRA, -1) + + val config = LeakCanary.config + // 将解析结果保存到HeapAnalysis中 + val heapAnalysis = if (heapDumpFile.exists()) { + // 解析该文件 + analyzeHeap(heapDumpFile, config) + } else { + missingFileFailure(heapDumpFile) + } + val fullHeapAnalysis = when (heapAnalysis) { + is HeapAnalysisSuccess -> heapAnalysis.copy( + dumpDurationMillis = heapDumpDurationMillis, + metadata = heapAnalysis.metadata + ("Heap dump reason" to heapDumpReason) + ) + is HeapAnalysisFailure -> heapAnalysis.copy(dumpDurationMillis = heapDumpDurationMillis) + } + onAnalysisProgress(REPORTING_HEAP_ANALYSIS) + // 将解析结果回调回去 + config.onHeapAnalyzedListener.onHeapAnalyzed(fullHeapAnalysis) + } +``` + +继续查看analyzeHeap() : + +```kotlin + private fun analyzeHeap( + heapDumpFile: File, + config: Config + ): HeapAnalysis { + val heapAnalyzer = HeapAnalyzer(this) + + val proguardMappingReader = try { + ProguardMappingReader(assets.open(PROGUARD_MAPPING_FILE_NAME)) + } catch (e: IOException) { + null + } + return heapAnalyzer.analyze( + heapDumpFile = heapDumpFile, + leakingObjectFinder = config.leakingObjectFinder, + referenceMatchers = config.referenceMatchers, + computeRetainedHeapSize = config.computeRetainedHeapSize, + objectInspectors = config.objectInspectors, + metadataExtractor = config.metadataExtractor, + proguardMapping = proguardMappingReader?.readProguardMapping() + ) + } +``` + +继续使用Shark库中的HeapAnalyzer.analyze()方法: + +```kotlin +fun analyze( + heapDumpFile: File, + leakingObjectFinder: LeakingObjectFinder, + referenceMatchers: List = emptyList(), + computeRetainedHeapSize: Boolean = false, + objectInspectors: List = emptyList(), + metadataExtractor: MetadataExtractor = MetadataExtractor.NO_OP, + proguardMapping: ProguardMapping? = null + ): HeapAnalysis { + val analysisStartNanoTime = System.nanoTime() + + if (!heapDumpFile.exists()) { + val exception = IllegalArgumentException("File does not exist: $heapDumpFile") + return HeapAnalysisFailure( + heapDumpFile = heapDumpFile, + createdAtTimeMillis = System.currentTimeMillis(), + analysisDurationMillis = since(analysisStartNanoTime), + exception = HeapAnalysisException(exception) + ) + } + + return try { + listener.onAnalysisProgress(PARSING_HEAP_DUMP) + // 生成hprof文件中Record的关系图 + val sourceProvider = ConstantMemoryMetricsDualSourceProvider(FileSourceProvider(heapDumpFile)) + sourceProvider.openHeapGraph(proguardMapping).use { graph -> + val helpers = + FindLeakInput(graph, referenceMatchers, computeRetainedHeapSize, objectInspectors) + // 构建从GC Roots到监测对象的最短引用路径,并返回结果 + val result = helpers.analyzeGraph( + metadataExtractor, leakingObjectFinder, heapDumpFile, analysisStartNanoTime + ) + val lruCacheStats = (graph as HprofHeapGraph).lruCacheStats() + val randomAccessStats = + "RandomAccess[" + + "bytes=${sourceProvider.randomAccessByteReads}," + + "reads=${sourceProvider.randomAccessReadCount}," + + "travel=${sourceProvider.randomAccessByteTravel}," + + "range=${sourceProvider.byteTravelRange}," + + "size=${heapDumpFile.length()}" + + "]" + val stats = "$lruCacheStats $randomAccessStats" + result.copy(metadata = result.metadata + ("Stats" to stats)) + } + } catch (exception: Throwable) { + HeapAnalysisFailure( + heapDumpFile = heapDumpFile, + createdAtTimeMillis = System.currentTimeMillis(), + analysisDurationMillis = since(analysisStartNanoTime), + exception = HeapAnalysisException(exception) + ) + } + } +``` + +```kotlin +private fun FindLeakInput.analyzeGraph( + metadataExtractor: MetadataExtractor, + leakingObjectFinder: LeakingObjectFinder, + heapDumpFile: File, + analysisStartNanoTime: Long + ): HeapAnalysisSuccess { + listener.onAnalysisProgress(EXTRACTING_METADATA) + val metadata = metadataExtractor.extractMetadata(graph) + + val retainedClearedWeakRefCount = KeyedWeakReferenceFinder.findKeyedWeakReferences(graph) + .filter { it.isRetained && !it.hasReferent }.count() + + // This should rarely happens, as we generally remove all cleared weak refs right before a heap + // dump. + val metadataWithCount = if (retainedClearedWeakRefCount > 0) { + metadata + ("Count of retained yet cleared" to "$retainedClearedWeakRefCount KeyedWeakReference instances") + } else { + metadata + } + + listener.onAnalysisProgress(FINDING_RETAINED_OBJECTS) + val leakingObjectIds = leakingObjectFinder.findLeakingObjectIds(graph) + + val (applicationLeaks, libraryLeaks, unreachableObjects) = findLeaks(leakingObjectIds) + + return HeapAnalysisSuccess( + heapDumpFile = heapDumpFile, + createdAtTimeMillis = System.currentTimeMillis(), + analysisDurationMillis = since(analysisStartNanoTime), + metadata = metadataWithCount, + applicationLeaks = applicationLeaks, + libraryLeaks = libraryLeaks, + unreachableObjects = unreachableObjects + ) + } +``` + + + + + + + + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/SourceAnalysis/butterknife\346\272\220\347\240\201\350\257\246\350\247\243.md" "b/SourceAnalysis/butterknife\346\272\220\347\240\201\350\257\246\350\247\243.md" index ada7be25..dfe613b2 100644 --- "a/SourceAnalysis/butterknife\346\272\220\347\240\201\350\257\246\350\247\243.md" +++ "b/SourceAnalysis/butterknife\346\272\220\347\240\201\350\257\246\350\247\243.md" @@ -261,7 +261,7 @@ private Map findAndParseTargets(RoundEnvironment env) return targetClassMap; } ``` - + 继续看一下`parseBindView()`方法: ```java private void parseBindView(Element element, Map targetClassMap, @@ -484,7 +484,7 @@ static final Map, ViewBinder> BINDERS = new LinkedHashMap<>(); 那`$_ViewBinder`类里面都是什么内容呢? 我们去看一下该类的代码,但是它生成的代码在哪里呢? ![image](https://github.com/CharonChui/Pictures/blob/master/butterknife_apt_genierate_code.png?raw=true) - + 开始看一下`SimpleActivity_ViewBinder.bind()`方法: ```java @@ -565,7 +565,7 @@ public class SimpleActivity_ViewBinding implements Unb } ``` 可以看到他内部会通过`findViewByid()`等来找到对应的`View`,然后将其赋值给`target.xxxx`,所以这样就相当于把所有的控件以及事件都给初始化了,以后就可以直接使用了,通过这里也可以看到我们在使用注解的时候不要把控件或者方法声明为`private`的。 - + 总结一下: diff --git "a/VideoDevelopment/\345\205\263\351\224\256\345\270\247.md" "b/VideoDevelopment/\345\205\263\351\224\256\345\270\247.md" index 5366ba8d..a3884aec 100644 --- "a/VideoDevelopment/\345\205\263\351\224\256\345\270\247.md" +++ "b/VideoDevelopment/\345\205\263\351\224\256\345\270\247.md" @@ -33,7 +33,7 @@ GOP(Group of Pictures)是一组连续的画面,由一张I帧和数张B/P帧组 - IDR(Instantaneous Decoding Refresh)--即时解码刷新。 I帧:帧内编码帧是一种自带全部信息的独立帧,无需参考其它图像便可独立进行解码,视频序列中的第一个帧始终都是I帧。 - 那么IDR帧与I帧的区别是什 么呢?因为H264采用了多帧预测,所以I帧之后的P帧有可能会参考I 帧之前的帧,这就使得在随机访问的时候不能以找到I帧作为参考条 件,因为即使找到I帧,I帧之后的帧还是有可能解析不出来,而IDR帧 就是一种特殊的I帧,即这一帧之后的所有参考帧只会参考到这个IDR 帧,而不会再参考前面的帧。在解码器中,一旦收到一个IDR帧,就会 立即清理参考帧缓冲区,并将IDR帧作为被参考的帧。 + 那么IDR帧与I帧的区别是什么呢?因为H264采用了多帧预测,所以I帧之后的P帧有可能会参考I 帧之前的帧,这就使得在随机访问的时候不能以找到I帧作为参考条 件,因为即使找到I帧,I帧之后的帧还是有可能解析不出来,而IDR帧 就是一种特殊的I帧,即这一帧之后的所有参考帧只会参考到这个IDR 帧,而不会再参考前面的帧。在解码器中,一旦收到一个IDR帧,就会 立即清理参考帧缓冲区,并将IDR帧作为被参考的帧。 From 5b27ac2733fdc43cd7822a1c70aeb1932e242309 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 16 Mar 2021 21:10:29 +0800 Subject: [PATCH 047/213] update README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cb8b3b84..1a456b7f 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Android学习笔记 - [ListView源码分析][8] - [VideoView源码分析][9] - [View绘制过程详解][10] + - [LeakCanary源码分析][284] - [网络部分][11] - [HttpURLConnection详解][12] - [HttpURLConnection与HttpClient][13] @@ -601,7 +602,7 @@ Android学习笔记 [283]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/9.PackageManagerService%E7%AE%80%E4%BB%8B.md "9.PackageManagerService简介" - +[ 284 ]: https://github.com/CharonChui/AndroidNote/blob/master/SourceAnalysis/LeakCanary%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md. "LeakCanary源码分析" Developed By From 085c3030829e7bf8d749321ad62e96a0bd874608 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 19 Mar 2021 19:17:54 +0800 Subject: [PATCH 048/213] update --- ...27\256\351\242\230\345\210\206\346\236\220.md" | 15 ++++++++++++++- "JavaKnowledge/Git\347\256\200\344\273\213.md" | 11 +++++------ ...63\273\347\273\237\347\256\200\344\273\213.md" | 8 ++++++++ ...50\213\344\270\216\347\272\277\347\250\213.md" | 4 ++-- ...66\210\346\201\257\346\234\272\345\210\266.md" | 6 +++++- ...73\230\345\210\266\345\237\272\347\241\200.md" | 14 ++++++++++++++ 6 files changed, 48 insertions(+), 10 deletions(-) diff --git "a/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" "b/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" index 793904c7..e7dcaaa0 100644 --- "a/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" +++ "b/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" @@ -5,6 +5,19 @@ OOM问题分析 OOM(OutOfMemoryError),最近线上版本出现了大量线程OOM的crash,尤其是华为Android 9.0系统的手机,占总OOM量的85%左右。 + + +## 内存指标概念 + + + +- USS(Unique Set Size): 物理内存,进程独占的内存 +- PSS(Proportional Set Size): 物理内存,PSS = USS + 按比例包含共享库 +- RSS(Resident Set Size): 物理内存,RSS = USS + 包含共享库 +- VSS(Virtual Set Size): 虚拟内存,VSS = RSS + 未分配实际物理内存 + + + ### OOM分类 #### [XXXClassName] of length XXX would overflow“是系统限制String/Array的长度所致,这种情况比较少。 @@ -761,7 +774,7 @@ nonvoluntary_ctxt_switches: 328 ``` 当线程数(可以在/proc/pid/status 中的threads项实时查看)超过/proc/sys/kernel/threads-max 中规定的上限时产生 OOM 崩溃。 ``` - + ## 定位验证方法: Thread.UncaughtExceptionHandler捕获到OutOfMemoryError时记录/proc/pid目录下的如下信息: diff --git "a/JavaKnowledge/Git\347\256\200\344\273\213.md" "b/JavaKnowledge/Git\347\256\200\344\273\213.md" index c1022154..2a33f604 100644 --- "a/JavaKnowledge/Git\347\256\200\344\273\213.md" +++ "b/JavaKnowledge/Git\347\256\200\344\273\213.md" @@ -91,7 +91,7 @@ git push // 把所有文件从本地仓库推送进远程仓库 ``` 先上一张图 -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git.jpg) +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git.png) 图中的`index`部分就是暂存区 - 安装好git后我们要先配置一下。以便`git`跟踪。 @@ -99,7 +99,7 @@ git push // 把所有文件从本地仓库推送进远程仓库 ``` git config --global user.name "xxx" git config --global user.email "xxx@xxx.com" - ``` + ``` 上面修改后可以使用`cat ~/.gitconfig`查看 如果指向修改仓库中的用户名时可以不加`--global`,这样可以用`cat .git/config`来查看 `git config --list`来查看所有的配置。 @@ -135,7 +135,6 @@ git push // 把所有文件从本地仓库推送进远程仓库 简单用法: `git cherry-pick ` - - `git status`查看当前仓库的状态和信息,会提示哪些内容做了改变已经当前所在的分支。 - `git diff` @@ -172,7 +171,7 @@ git push // 把所有文件从本地仓库推送进远程仓库 - `–after` ——显示某个日期之后发生的提交 - `–before` ——显示发生某个日期之前的提交 - + - `git reflog` 可以查看所有操作记录包括`commit`和`reset`操作以及删除的`commit`记录 @@ -249,7 +248,7 @@ git push // 把所有文件从本地仓库推送进远程仓库 ``` git reset // git reset 只是把修改退回到了git add .之前的状态,也就是让文件还处于已修改未暂存的状态 git checkout . // 上面让文件处于已修改未暂存的状态,还要执行git checkout .来撤销工作区的状态 - ``` + ``` 或`git reset --hard` 上面两个例子中都使用了`git reset --hard`这个命令也可以完成,这个命令可以一步到位的把你的修改完全恢复到本地仓库的未修改的状态。 @@ -473,5 +472,5 @@ $ git log --graph --pretty=oneline --abbrev-commit - +​ diff --git "a/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" index 0b4d80e4..d7e13d3a 100644 --- "a/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" +++ "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" @@ -523,7 +523,15 @@ int main(int argc, char *argv[]) { +## Linux操作系统特点 +Linux是类Unix系统,借鉴了Unix的设计并实现相关接口,但并非Unix。Linux是由Linus Torvalds于1991年创造的开源免费系统,采用GNU GPL协议保护,下面列举Linux的一些主要特点: + +- Linux系统中万物皆为文件,这种抽象方便操作数据或设备,只需一套统一的系统接口open, read, write, close即可完成对文件的操作 +- Linux是单内核,支持动态加载内核模块,可在运行时根据需求动态加载和卸载部分内核代码; +- Linux内核支持可抢占; +- Linux内核创建进程,采用独特的fork()系统调用,创建进程较高效; +- Linux内核并不区分进程和线程,对于内核而言,进程与线程无非是共享资源的区别,对CPU调度来说并没有显著差异。 diff --git "a/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" index 9801b918..7cf76645 100644 --- "a/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" +++ "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" @@ -529,8 +529,8 @@ Android使用的进程有些不同。活动管理器是Android负责正在运行 2. 引导程序BootLoader:BootLoader是在Android系统开始运行前的一个小程序,主要用于把系统OS拉起来并运行。 3. Linux内核启动:当内核启动时,设置缓存、被保护存储器、计划列表、加载驱动。当其完成系统设置时,会先在系统文件中寻找init.rc文件,并启动init进程。 -4. init进程启动:初始化和启动属性服务,init进程会孵化出eadbd、logd、等用户守护进程,还会启动ServiceManager(binder服务管家)、botanic(开机动画)等服务,并且启动Zygote进程。 -5. Zygote进程启动:zygote进程是Android系统的第一个Java进程(即虚拟机进程),它是所有Java进程的父进程。它会创建JVM并为其注册JNI方法,创建服务器端Socket,启动SystemServer进程。并且,zygote进程在启动的时候会创建DVM或者ART。因此通过从zygote进程fork创建的应用程序进程和systemserver进程都可以在内部获取一个DVM或者ART的实例副本。它还会提前加载类preloadClasses和提前加载资源preloadResouces。 +4. init进程启动(是所有用户进程的父进程(或者父父进程)):初始化和启动属性服务,init进程会孵化出eadbd、logd、等用户守护进程,还会启动ServiceManager(binder服务管家)、botanic(开机动画)等服务,并且启动Zygote进程。 +5. Zygote进程启动(zygote是所有上层Java进程的父进程,zygote的父进程是init进程):zygote进程是Android系统的第一个Java进程(即虚拟机进程),它是所有Java进程的父进程。它会创建JVM并为其注册JNI方法,创建服务器端Socket,启动SystemServer进程。并且,zygote进程在启动的时候会创建DVM或者ART。因此通过从zygote进程fork创建的应用程序进程和systemserver进程都可以在内部获取一个DVM或者ART的实例副本。它还会提前加载类preloadClasses和提前加载资源preloadResouces。 6. SystemServer进程启动:System Server是zygote孵化的第一个进程,它会启动Binder线程池和SystemServiceManager,并且启动各种系统服务,包括ActivityManagerService、WindowManagerService、PackageManagerService、PowerManagerService等服务。 7. Media Server进程,是由init进程fork而来,负责启动和管理整个C++ framework,包括AudioFlinger,Camera Service等服务。 8. Launcher启动:是zygote孵化的第一个App进程,被SystemServer进程启动的AmS会启动Launcher,Launcher启动后会将已安装应用的快捷图标显示到系统桌面上。zygote还会创建Broweer、Phone、Email等App进程,每个App至少运行在一个进程上。 diff --git "a/OperatingSystem/AndroidKernal/2.Android\347\272\277\347\250\213\351\227\264\351\200\232\344\277\241\344\271\213Handler\346\266\210\346\201\257\346\234\272\345\210\266.md" "b/OperatingSystem/AndroidKernal/2.Android\347\272\277\347\250\213\351\227\264\351\200\232\344\277\241\344\271\213Handler\346\266\210\346\201\257\346\234\272\345\210\266.md" index dbcaf9bb..ba1a361e 100644 --- "a/OperatingSystem/AndroidKernal/2.Android\347\272\277\347\250\213\351\227\264\351\200\232\344\277\241\344\271\213Handler\346\266\210\346\201\257\346\234\272\345\210\266.md" +++ "b/OperatingSystem/AndroidKernal/2.Android\347\272\277\347\250\213\351\227\264\351\200\232\344\277\241\344\271\213Handler\346\266\210\346\201\257\346\234\272\345\210\266.md" @@ -19,6 +19,10 @@ Binder/Socket用于进程间通信,而Handler消息机制用于同进程的线 - Looper:不断循环执行(Looper.loop),按分发机制将消息分发给目标处理者。Looper有一个MessageQueue消息队列; + +![handle_msg_arch](https://raw.githubusercontent.com/CharonChui/Pictures/master/handle_msg_arch.png) + + 首先想一想平时我们是怎么使用的: ```java private Handler mHandler = new Handler(new Handler.Callback() { @@ -866,7 +870,7 @@ public class SampleActivity extends Activity { - `removeCallbacksAndMessages(Object token)` ——清除所有callback以及token匹配上的Message,如果token是null就会清楚所有callback和message。我们更多需要的是清除以该`Handler`为`target`的所有`Message(Callback)`就调用如下方法即可`handler.removeCallbacksAndMessages(null)`; - `removeMessages(int what)` ——按what来匹配 - `removeMessages(int what, Object object)` ——按what来匹配 - + - 将`Handler`声明为静态类。 静态类不持有外部类的对象,所以你的`Activity`可以随意被回收。但是不持有`Activity`的引用,如何去操作`Activity`中的一些对象? 这里要用到弱引用 ```java diff --git "a/OperatingSystem/AndroidKernal/6.\345\261\217\345\271\225\347\273\230\345\210\266\345\237\272\347\241\200.md" "b/OperatingSystem/AndroidKernal/6.\345\261\217\345\271\225\347\273\230\345\210\266\345\237\272\347\241\200.md" index 620d6f10..e24311b8 100644 --- "a/OperatingSystem/AndroidKernal/6.\345\261\217\345\271\225\347\273\230\345\210\266\345\237\272\347\241\200.md" +++ "b/OperatingSystem/AndroidKernal/6.\345\261\217\345\271\225\347\273\230\345\210\266\345\237\272\347\241\200.md" @@ -66,11 +66,25 @@ Framebuffer是系内核系统提供的图形硬件的抽象描述。之所以称 +无论开发者使用什么渲染 API,一切内容都会渲染到“Surface”。Surface 表示缓冲队列中的生产方,而缓冲队列通常会被 SurfaceFlinger 消耗。在 Android 平台上创建的每个窗口都由 Surface 提供支持。所有被渲染的可见 Surface 都被 SurfaceFlinger 合成到显示部分。 +### 图像流生产方 +图像流生产方可以是生成图形缓冲区以供消耗的任何内容。例如 OpenGL ES、Canvas 2D 和 mediaserver 视频解码器。 +### 图像流消耗方 + +图像流的最常见消耗方是 SurfaceFlinger,该系统服务会消耗当前可见的 Surface,并使用窗口管理器中提供的信息将它们合成到显示部分。SurfaceFlinger 是可以修改所显示部分内容的唯一服务。SurfaceFlinger 使用 OpenGL 和 Hardware Composer 来合成一组 Surface。 + +其他 OpenGL ES 应用也可以消耗图像流,例如相机应用会消耗相机预览图像流。非 GL 应用也可以是使用方,例如 ImageReader 类。 + +### 硬件混合渲染器 + +显示子系统的硬件抽象实现。SurfaceFlinger 可以将某些合成工作委托给 Hardware Composer,以分担 OpenGL 和 GPU 上的工作量。SurfaceFlinger 只是充当另一个 OpenGL ES 客户端。因此,在 SurfaceFlinger 将一个或两个缓冲区合成到第三个缓冲区中的过程中,它会使用 OpenGL ES。这样使合成的功耗比通过 GPU 执行所有计算更低。 + +[Hardware Composer HAL](https://source.android.com/devices/graphics/architecture#hwcomposer) 则进行另一半的工作,并且是所有 Android 图形渲染的核心。Hardware Composer 必须支持事件,其中之一是 VSYNC(另一个是支持即插即用 HDMI 的热插拔)。 From 77f48f87b66aac56e896ace05a8ff479e54655d8 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 2 Apr 2021 22:03:37 +0800 Subject: [PATCH 049/213] update kotlin note --- ...&\347\261\273&\346\216\245\345\217\243.md" | 1050 +++++++++++++++++ ...76\350\256\241\346\250\241\345\274\217.md" | 458 +++++++ ...05\350\201\224\345\207\275\346\225\260.md" | 742 ++++++++++++ ...0\347\273\204&\351\233\206\345\220\210.md" | 554 +++++++++ ...7&\345\205\263\351\224\256\345\255\227.md" | 224 ++++ ...2\344\270\276&\345\247\224\346\211\230.md" | 216 +++- ...47\346\211\277\351\227\256\351\242\230.md" | 211 ++++ ...5\345\260\204&\346\211\251\345\261\225.md" | 859 ++++++++++++++ .../8.Kotlin_\345\215\217\347\250\213.md" | 97 ++ ...\346\225\231\347\250\213(\344\270\200).md" | 534 --------- ...\346\225\231\347\250\213(\344\270\203).md" | 96 -- ...\346\225\231\347\250\213(\344\270\211).md" | 196 --- ...\346\225\231\347\250\213(\344\271\235).md" | 8 + ...\346\225\231\347\250\213(\344\272\214).md" | 88 -- ...\346\225\231\347\250\213(\345\205\253).md" | 121 +- ...\346\225\231\347\250\213(\345\205\255).md" | 204 ---- ...\346\225\231\347\250\213(\345\215\201).md" | 35 +- ...\346\225\231\347\250\213(\345\233\233).md" | 581 --------- 18 files changed, 4421 insertions(+), 1853 deletions(-) create mode 100644 "KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" create mode 100644 "KotlinCourse/11.Kotlin_\350\256\276\350\256\241\346\250\241\345\274\217.md" create mode 100644 "KotlinCourse/2.Kotlin_\351\253\230\351\230\266\345\207\275\346\225\260&Lambda&\345\206\205\350\201\224\345\207\275\346\225\260.md" create mode 100644 "KotlinCourse/3.Kotlin_\346\225\260\345\255\227&\345\255\227\347\254\246\344\270\262&\346\225\260\347\273\204&\351\233\206\345\220\210.md" create mode 100644 "KotlinCourse/4.Kotlin_\350\241\250\350\276\276\345\274\217&\345\205\263\351\224\256\345\255\227.md" rename "KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\272\224).md" => "KotlinCourse/5.Kotlin_\345\206\205\351\203\250\347\261\273&\345\257\206\345\260\201\347\261\273&\346\236\232\344\270\276&\345\247\224\346\211\230.md" (62%) create mode 100644 "KotlinCourse/6.Kotlin_\345\244\232\347\273\247\346\211\277\351\227\256\351\242\230.md" create mode 100644 "KotlinCourse/7.Kotlin_\346\263\250\350\247\243&\345\217\215\345\260\204&\346\211\251\345\261\225.md" create mode 100644 "KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" delete mode 100644 "KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\200).md" delete mode 100644 "KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\203).md" delete mode 100644 "KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\211).md" delete mode 100644 "KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\272\214).md" delete mode 100644 "KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\255).md" delete mode 100644 "KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\233\233).md" diff --git "a/KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" "b/KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" new file mode 100644 index 00000000..b4432c22 --- /dev/null +++ "b/KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" @@ -0,0 +1,1050 @@ +1.Kotlin_简介 +=== + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_kotlin.jpeg?raw=true) + +在`5月18`日谷歌在`I/O`开发者大会上宣布,将`Kotlin`语言作为安卓开发的一级编程语言。并且会在`Android Studio 3.0`版本全面支持`Kotlin`。 + +- `Kotlin`是一个基于`JVM`的新的编程语言,由[JetBrains](https://www.jetbrains.com/)开发。`JetBrains`作为目前广受欢迎的 +`Java IDE IntelliJ`的提供商,在`Apache`许可下已经开源其`Kotlin`编程语言。 +- `Kotlin`可以编译成`Java`字节码,也可以编译成`JavaScript`,方便在没有`JVM`的设备上运行。 +- `Kotlin`已正式成为`Android`官方开发语言。 + +[Kotlin官网](https://kotlinlang.org/) + +`JetBrains`这家公司非常牛逼,开发了很多著名的软件,他们在使用`Java`的过程中发现`java`比较笨重不方便,所以就开发了`kotlin`,`kotlin`是 +一种全栈的开发语言,可以用它进行开发`web`、`web`后端、`Android`等。 + +很多开发者都说`Google`学什么不好,非要学苹果,出个`android`的`swift`版本,一定会搞不起来没人用,所以不用浪费时间去学习。在这里想引用马云 +的一句话: +> 拥抱变化 + +`Google`做事,向来言出必行,之前在推行`Android Studio`时也是一片骂声,吐槽各种不好用,各种慢。但是现在`Android Studio`基本都已经普及了。 +我相信`Kotlin`也不会例外。所以我们不仅要学,还要要认真的学。 + +## `Kotlin`的特性 + +- 它更加易表现:这是它最重要的优点之一。你可以编写少得多的代码。 +- `Kotlin`是一种兼容`Java`的语言 +- `Kotlin`比`Java`更安全,能够静态检测常见的陷阱。如:引用空指针 +- `Kotlin`比`Java`更简洁,通过支持`variable type inference,higher-order functions (closures),extension functions,mixins +and first-class delegation`等实现 +- `Kotlin`可与`Java`语言无缝通信。这意味着我们可以在`Kotlin`代码中使用任何已有的`Java`库;同样的`Kotlin`代码还可以为`Java`代码所用 +- `Kotlin`在代码中很少需要在代码中指定类型,因为编译器可以在绝大多数情况下推断出变量或是函数返回值的类型。这样就能获得两个好处:简洁与安全 + +## `Kotlin`优势 + +- 全面支持`Lambda`表达式 +- 数据类`Data classes` +- 函数字面量和内联函数`Function literals & inline functions` +- 函数扩展`Extension functions` +- 空安全`Null safety` +- 智能转换`Smart casts` +- 字符串模板`String templates` +- 主构造函数`Primary constructors` +- 类委托`Class delegation` +- 类型推判`Type inference` +- 单例`Singletons` +- 声明点变量`Declaration-site variance` +- 区间表达式`Range expressions` + + +上面说简洁简洁,到底简洁在哪里?这里先用一个例子开始,在`Java`开发过程中经常会写一些`Bean`类: +```java +package com.charon.kotlinstudydemo; + +public class Person { + private int age; + private String name; + private float height; + private float weight; + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public float getHeight() { + return height; + } + + public void setHeight(float height) { + this.height = height; + } + + public float getWeight() { + return weight; + } + + public void setWeight(float weight) { + this.weight = weight; + } + + @Override + public String toString() { + return "Person name is : " + name + " age is : " + age + " height is :" + + height + " weight is :" + weight; + } +} +``` +使用`Kotlin`: +```kotlin +package com.charon.kotlinstudydemo + +data class Person( + var name: String, + var age: Int, + var height: Float, + var weight: Float) +``` +这个数据类,它会自动生成所有属性和它们的访问器,以及一些有用的方法,比如`toString()`方法。 +这里插一嘴,从上面的例子中我们可以看到对于包的声明基本是一样的,唯一不同的是`kotlin`中后面结束不用分号。 + +## 创建`Kotlin`项目 + +`Google`宣布在`Android Studio 3.0`版本会全面支持`Kotlin`,目前早就有预览版了 +[Android Studio Preview](https://developer.android.com/studio/preview/index.html)(个人感觉很好用,比2.3.3版本强多了)。 +直接通过`New Project`创建就可以,与创建普通`Java`项目唯一不同的是要勾选`Include Kotlin support`的选项。 + + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/studio_create_kotlin.png?raw=true) + +创建完成后我们看一下`MainActivity`的代码: +```kotlin +// 定义包 +package com.charon.kotlinstudydemo + +// 导入 +import android.support.v7.app.AppCompatActivity +import android.os.Bundle + +// 定义类,继承AppCompatActivity +class MainActivity : AppCompatActivity() { + + // 重写方法用overide,函数名用fun声明 参数是a: 类型的形式 ?是啥?它是指明该对象可能为null, + // 如果有了?那在调用该方法的时候参数可以传递null进入,如果没有?传递null就会报错 + override fun onCreate(savedInstanceState: Bundle?) { + // super + super.onCreate(savedInstanceState) + // 调用方法 + setContentView(R.layout.activity_main) + } +} +``` + +我们就从`MainActivity`的代码开始介绍一些基本的语法。 + +## 变量 + +变量可以很简单地定义成可变`var`(可读可写)和不可变`val`(只读)的变量。如果var代表了varible(变量),那么val可看成value(值)的缩写,但是也有人觉得这样并不直观或准确,而是把val解释成varible+final,即通过val声明的变量具有Java中的final关键字的效果(我们通过查看对val语法反编译后转化的java代码,从中可以很清楚的发现它是用final实现的。),也就是引用不可变。因此,val声明的变量是只读变量,它的引用不可更改,但并不代表其引用对象也不可变。事实上,我们依然可以修改引用对象的可变成员。 + +声明: +```kotlin +var age: Int = 18 +val name: String = "charon" + +val book = Book("Thinking in Java") // 用val声明的book对象的引用不可变 +book.name = "Diving into Kotlin" +book.printName() // Diving into Kotlin +``` + +再提示一下:`kotlin`中每行代码结束不需要分号了,不要和`java`是的每行都带分号 + +字面上可以写明具体的类型。这个不是必须的,但是一个通用的`Kotlin`实践时省略变量的类型我们可以让编译器自己去推断出具体的类型: +```kotlin +var age = 18 // int +val name = "charon" // string +var height = 180.5f // flat +var weight = 70.5 // double +``` + +在`Kotlin`中,一切都是对象。没有像`Java`中那样的原始基本类型。 +当然,像`Integer`,`Float`或者`Boolean`等类型仍然存在,但是它们全部都会作为对象存在的。基本类型的名字和它们工作方式都是与`Java`非常相似 +的,但是有一些不同之处你可能需要考虑到: + +- 数字类型中不会自动转型。举个例子,你不能给`Double`变量分配一个`Int`。必须要做一个明确的类型转换,可以使用众多的函数之一: + ```kotlin + private var age = 18 + private var weight = age.toFloat() + ``` +- 字符(`Char`)不能直接作为一个数字来处理。在需要时我们需要把他们转换为一个数字: + ```kotlin + val c: Char='c' + val i: Int = c.toInt() + ``` +- 位运算也有一点不同。在`Android`中,我们经常在`flags`中使用`或`: + ```java + // Java + int bitwiseOr = FLAG1 | FLAG2; + int bitwiseAnd = FLAG1 & FLAG2; + ``` + + ```kotlin + // Kotlin + val bitwiseOr = FLAG1 or FLAG2 + val bitwiseAnd = FLAG1 and FLAG2 + ``` + +- 一个`String`可以像数组那样访问,并且被迭代: + ```kotlin + var s = "charon" + var c = s[2] + + for (a in s) { + Log.e("@@@", a +""); + } + ``` + +### 优先使用val来避免副作用 + +在很多Kotlin的学习资料中,都会传递一个原则:优先使用val来声明变量。这相当正确,但更好的理解可以是:尽可能采用val、不可变对象及纯函数来设计程序。关于纯函数的概念,其实就是没有副作用的函数,具备引用透明性。 + +简单来说,副作用就是修改了某处的某些东西,比如说: + +- 修改了外部变量的值 +- IO操作,如写数据到磁盘 +- UI操作,如修改了一个按钮的可操作状态 + +来看一个实际的例子:先用va来声明一个变量a,然后在count函数内部对其进行自增操作: + +```kotlin +val a = 1 +fun count(x: Int) { + a = a + 1 + println(x + a) +} +``` + +如果执行两次count(1)函数,第一次的执行结果是3、第二次的执行结果是4。这显然是受到了外部变量a的影响,这个就是典型的副作用。 + + + +## 编译期常量 + +已知值的属性可以使用`const`修饰符标记为编译期常量(类似`java`中的`public static final`)。 +`const`只能修复`val`不能修复`var`,这些属性需要满足以下要求: +- 位于顶层或者是`object`的一个成员 +- 用`String`或原生类型值初始化 +- 没有自定义`getter` + +```kotlin +// Const val are only allowed on top level or in objects +const val NAME: String = "charon" + +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + } +} +``` + +## 后端变量`Backing Fields`. + +在`kotlin`的`getter`和`setter`是不允许本身的局部变量的,因为属性的调用也是对`get`的调用,因此会产生递归,造成内存溢出。 + +例如: + +```kotlin +var count = 1 +var size: Int = 2 +set(value) { + Log.e("text", "count : ${count++}") + size = if (value > 10) 15 else 0 +} +``` +这个例子中就会内存溢出。 + +`kotlin`为此提供了一种我们要说的后端变量,也就是`field`。编译器会检查函数体,如果使用到了它,就会生成一个后端变量,否则就不会生成。 +我们在使用的时候,用`field`代替属性本身进行操作。 + +## 延迟初始化 + +我们说过,在类内声明的属性必须初始化,如果设置非`null`的属性,应该将此属性在构造器内进行初始化。 +假如想在类内声明一个`null`属性,在需要时再进行初始化(最典型的就是懒汉式单例模式),与`Kotlin`的规则是相背的,此时我们可以声明一个属性并 +延迟其初始化,此属性用`lateinit`修饰符修饰。 + +```kotlin +class MainActivity : AppCompatActivity() { + lateinit var name : String + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + var test = MainActivity() + // 要先调用方法让其初始化 + test.init() + // 再使用其属性 + Log.e("@@@", test.name) + } + + fun init() { + // 延迟初始化 + name = "charon" + } +} +``` +需要注意的是,我们在使用的时候,一定要确保属性是被初始化过的,通常先调用初始化方法,否则会有异常。 +如果只是用`lateinit`声明了,但是还没有调用初始化方法就使用,哪怕你判断了该变量是否为`null`也是会`crash`的。 +```kotlin +private lateinit var test: String + +private fun switchFragment(position: Int) { + if (test == null) { + LogUtil.e("@@@", "test is null") + } else { + LogUtil.e("@@@", "test is not null") + check(test) + } +} +``` +会报`kotlin.UninitializedPropertyAccessException: lateinit property test has not been initialized` + +除了使用`lateinit`外还可以使用`by lazy {}`效果是一样的: +```kotlin +private val test by lazy { "haha" } + +private fun switchFragment(position: Int) { + if (test == null) { + LogUtil.e("@@@", "test is null") + } else { + LogUtil.e("@@@", "test is not null ${test}") + check(test) + } +} +``` +执行结果: +``` +test is not null haha +``` + +那`lateinit`和`by lazy`有什么区别呢? + +- `by lazy{}`只能用在`val`类型而`lateinit`只能用在`var`类型 +- `lateinit`不能用在可空的属性上和`java`的基本类型上,否则会报`lateinit`错误 + +lazy的背后是接受一个lambda并返回一个Lazy实例的函数,第一次访问该属性时,会执行lazy对应的Lambda表达式并记录结果,后续访问该属性时只是返回记录的结果。 + +另外系统会给lazy属性默认加上同步锁,也就是LazyThreadSafetyMode.SYNCHRONIZED,它在同一时刻只允许一个线程对lazy属性进行初始化,所以它是线程安全的。但若你能确认该属性可以并行执行,没有线程安全问题,那么可以给lazy传递LazyThreadSafetyMode.PUBLICATION参数。你还可以给lazy传递LazyThreadSafetyMode.NONE参数,这将不会有任何线程方面的开销,当然也不会有任何线程安全的保证。例如: + +```kotlin +val sex: String by lazy(LazyThreadSafetyMode.PUBLICATION) { + // 并行模式 + if (color == "yellow") "male" else "female" +} + +val sex: String by lazy(LazyThreadSafetyMode.NONE) { + // 不做任何线程保证也不会有任何线程开销 + if (color == "yellow") "male" else "female" +} +``` + + + +## 类的定义:使用`class`关键字 + +类可以包含: +- 构造函数和初始化块 +- 函数 +- 属性 +- 嵌套类和内部类 +- 对象声明 + + +```kotlin +class MainActivity{ + +} +``` + +如果有参数的话你只需要在类名后面写上它的参数,如果这个类没有任何内容可以省略大括号: +```kotlin +class Person(name: String, age: Int) +``` + +### 创建类的实例 + +```kotlin +val person = Person("charon", 18) +``` + +上面的类有一个默认的构造函数。 + +注意:创建类的实例不用`new`了啊。 + +### 构造函数 + +在`Kotlin`中的一个类可以有一个主构造函数和一个或多个次构造函数。 + +#### 主构造函数 + +主构造函数是类头的一部分:它跟在类名(和可选的类型参数)后: +```kotlin +class Person constructor(name: String, surname: String) { +} +``` +如果主构造函数没有任何注解或者可见性修饰符,可以省略`constructor`关键字: +```kotlin +class Person(name: String, surname: String) { +} +``` + +主构造函数不能包含任何的代码。初始化的代码可以放到以`init`关键字作为前缀的初始化块中: + +```kotlin +class Person constructor(name: String, surname: String) { + init { + print("name is $name and surname is $surname") + } +} +``` + +如果构造函数有注解或可见性修饰符,那么`constructor`关键字是必需的,并且这些修饰符在它前面: +```kotlin +class Person private @Inject constructor(name: String, surname: String) { + init { + print("name is $name and surname is $surname") + } +} +``` + +#### 次构造函数 + +类也可以声明前缀有`constructor`的次构造函数: +```kotlin +class Person{ + constructor(name: String) { + print("name is $name") + } +} +``` + +如果类有一个主构造函数,每个次构造函数都需要委托给主构造函数(不然会报错), 可以直接委托或者通过别的次构造函数间接委托。 +委托到同一个类的另一个构造函数用`this`关键字即可: +```kotlin +class Person constructor(name: String) { + constructor(name: String, surName: String) : this(name) { + Log.d("@@@", "name is : $name surName is : $surName") + } +} +``` +使用该对象: +```kotlin +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + Person("charon", "chui") + } +} +``` +就会在`logcat`上打印: +`09-20 16:51:19.738 6010-6010/com.charon.kotlinstudydemo D/@@@: name is : charon surName is : chui` + +如果一个非抽象类没有声明任何(主或次)构造函数,它会有一个生成的不带参数的主构造函数。构造函数的可见性是`public`。 +如果你不希望你的类有一个公有构造函数,你需要声明一个带有非默认可见性的空的主构造函数: + +```kotlin +class Person private constructor(name: String) { +} +``` + +#### 构造方法默认参数 + +```kotlin +class Bird(val weight: Double = 0.00, val age: Int = 0, val color: String = "blue") + +val bird1 = Bird(color = "black") +val bird2 = Bird(weight = 1000.00, color = "black") +``` + +上面在Bird类中使用了val或者var来声明构造方法的参数。这一方面代表了参数的引用可变性,另一方面也使得我们再构造类的语法上得到了简化。事实上,构造方法的参数名前当然可以没有val和var。然而带上它们之后就等价于在Bird类内部声明了一个同名的属性,我们可以用this来进行调用。比如,上面定义的Bird类就类似于一下实现: + +```kotlin +// 构造方法参数名前没有val +class Bird (weight: Double = 0.00, age: Int = 0, color: String = "blue"){ + val weight: Double + val age: Int + val color: String + init { + this.weight = weight // 构造方法参数可以在init语句中被调用 + this.age = age + this.color = color + } +} +``` + +#### init语句块 + +Kotlin引入了一种叫作init语句块的语法,它属于上述构造方法的一部分,两者在表现形式上确实分离的。Bird类的构造方法在类的外部,它只能对参数进行赋值。如果我们需要在初始化时进行其他的额外操作,那么我们就可以使用init语句块来执行。比如: + +```kotlin +class Bird(weight: Double, aget: Int, color: String) { + init { + println("the weight is ${weight}") + } +} +``` + +当没有val或者var的时候,构造函数的参数可以在init语句块被直接调用。除此之外,不能在其他地方使用。以下是一个错误的用法: + +```kotlin +class Bird(weight: Double, age: Int, color: String) { + fun printWeight() { + print(weight) // Unresolved reference: weight + } +} +``` + +事实上,我们的构造方法还可以拥有多个init,他们会在对象被创建时按照类中从上到下的顺序先后执行。例如: + +```kotlin +class Bird(weight: Double, aget: Int, color: String) { + val weight: Double + val age: Int + val color: String +} + +init { + this.weight = weight + this.age = age +} +init { + this.color = color +} + +``` + +可以发现,多个init语句块有利于进一步对初始化的操作进行职能分离,这在复杂的业务开发中显得特别有用。 + + + +## 数据类:使用`data class`定义 + +数据类是一种非常强大的类: + +```java +public class Artist { + private long id; + private String name; + private String url; + private String mbid; + + public long getId() { + return id; + } + + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getMbid() { + return mbid; + } + + public void setMbid(String mbid) { + this.mbid = mbid; + } + + @Override public String toString() { + return "Artist{" + + "id=" + id + + ", name='" + name + '\'' + + ", url='" + url + '\'' + + ", mbid='" + mbid + '\'' + + '}'; + } +} +``` + +使用`Kotlin`: + +```kotlin +data class Artist( + var id: Long, + var name: String, + var url: String, + var mbid: String) +``` + +通过数据类,会自动提供以下函数: + +- 所有属性的`get() set()`方法 +- `equals()` +- `hashCode()` +- `copy()` +- `toString()` +- 一系列可以映射对象到变量中的函数(后面再说)。 + +如果我们使用不可修改的对象,就像我们之前讲过的,假如我们需要修改这个对象状态,必须要创建一个新的一个或者多个属性被修改的实例。 +这个任务是非常重复且不简洁的。 + +举个例子,如果要修改`Person`类中`charon`的`age`: + +```kotlin +data class Person(val name: String, + val age: Int) +``` + +```kotlin +val charon = Person("charon", 18) +val charon2 = charon.copy(age = 19) +``` + +如上,我们拷贝了`charon`对象然后只修改了`age`的属性而没有修改这个对象的其它状态。 + +如果你要在Kotlin声明一个数据类,必须满足以下几点条件: + +- 数据类必须拥有一个构造方法,该方法至少包含一个参数,一个没有数据的数据类是没有任何用处的。 +- 与普通的类不同,数据类构造方法的参数强制使用var或者val进行声明 +- data class之前不能用abstract、open、sealed或者inner进行修饰 +- 在Kotlin 1.1版本前数据类只允许实现接口,之后的版本既可以实现接口也可以继承类 + + + + + +## 多声明 + +多声明,也可以理解为变量映射,这就是编译器自动生成的`componentN()`方法。 + +```kotlin +var personD = PersonData("PersonData", 20, "male") +var (name, age) = personD + + +Log.d("test", "name = $name, age = $age") + +//输出 +name = PersonData, age = 20 +``` + +上面的多声明,大概可以翻译成这样: + +```kotlin +var name = f1.component1() +var age = f1.component2() +``` + +## 继承 + +在`Kotlin`中所有类都有一个共同的超类`Any`,这对于没有超类型声明的类是默认超类: + +```kotlin +class Person // 从 Any 隐式继承 +``` + +`Any`不是`java.lang.Object`。它除了`equals()`、`hashCode()`和`toString()`外没有任何成员。 +`Kotlin`中所有的类默认都是不可继承的(`final`),为什么要这样设计呢?引用`Effective Java`书中的第17条:要么为继承而设计,并提供文档说明, +要么就禁止继承。所以我们只能继承那些明确声明`open`或者`abstract`的类:要声明一个显式的超类型,我们把类型放到类头的冒号之后: + +```kotlin +open class Person(num: Int) +// 继承 +class SuperPerson(num: Int) : Person(num) +``` + +如果该类有一个主构造函数,其基类必须用基类型的主构造函数参数就地初始化。 +如果类没有主构造函数,那么每个次构造函数必须使用`super`关键字初始化其基类型,或委托给另一个构造函数做到这一点。 +注意,在这种情况下,不同的次构造函数可以调用基类型的不同的构造函数: + +```kotlin +class MyView : View { + constructor(ctx: Context) : super(ctx) + constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs) +} +``` + +在Java中,类默认是可以被继承的,除非你主动加final修饰符。而在Kotlin中恰好相反,默认是不可被继承的,除非你主动加可以继承的修饰符,那便是open,如果不加open,那它在转化为Java代码时就是final的: + +```kotlin +class Bird { + val weight: Double = 500.0 + val color: String = "blue" + val age: Int = 1 + fun fly() {} +} +``` + +将Bird类编译后转换为Java的代码: + +```java +public final class Bird { + private final double weight = 500.0; + private final String color = "blue"; + private final int age = 1; + public final double getWeight() { + return this.weight; + } + public final String getColor() { + return this.color; + } + public final int getAge() { + return this.age; + } + public final void fly() { + + } +} +``` + + + +## 覆盖 + +##### 方法覆盖 + + +只能重写显示标注可覆盖的方法: + +```kotlin +open class Person(num: Int) { + open fun changeName(name: String) { + + } + + fun changeAge(age: Int) { + + } +} + +class SuperPerson(num: Int) : Person(num) { + override fun changeName(name: String) { + // 通过super关键字调用超类实现 + super.changeName(name) + } +} +``` + +`SuperPerson.changeName()`方法前面必须加上`override`标注,不然编译器将会报错。如果像上面`Person.changeAge()`方法没有标注`open`, +则子类中不能定义相同的方法: + +```kotlin +class SuperPerson(num: Int) : Person(num) { + override fun changeName(name: String) { + super.changeName(name) + } + + // 编译器报错 + fun changeAge(age: Int) { + + } + // 重载是可以的 + fun changeAge(name: String) { + + } + // 重载是可以的 + fun changeAge(age: Int, name: String) { + + } +} +``` + +标记为`override`的成员本身是开放的,也就是说,它可以在子类中覆盖。如果你想禁止再次覆盖,可以使用`final`关键字: + +```kotlin +open class SuperPerson(num: Int) : Person(num) { + final override fun changeName(name: String) { + super.changeName(name) + } +} +``` + +##### 属性覆盖 + +属性覆盖与方法覆盖类似,只能覆盖显示标明`open`的属性,并且要用`override`开头: + +```kotlin +open class Person(num: Int) { + open val name: String = "" + + open fun changeName(name: String) { + + } + + fun changeAge(age: Int) { + + } +} + +open class SuperPerson(num: Int) : Person(num) { + override val name: String + get() = super.name + + final override fun changeName(name: String) { + super.changeName(name) + } + +} +``` + +每个声明的属性可以由具有初始化器的属性或者具有`get`方法的属性覆盖,你也可以用一个`var`属性覆盖一个`val`属性,但反之则不行。 + + + +## 抽象类 + +类和其中的某些成员可以声明为`abstract`。抽象成员在本类中可以不用实现。 需要注意的是,我们并不需要用`open`标注一个抽象类或者函数——因为这不 +言而喻。 + +我们可以用一个抽象成员覆盖一个非抽象的开放成员: + +```kotlin +open class Base { + open fun f() {} +} + +abstract class Derived : Base() { + override abstract fun f() +} +``` + + + +## 注释 + +和`Java`差不多 + +```kotlin +// 这是一个行注释 + +/* 这是一个多行的 + 块注释。 */ +``` + + + + + + +## 接口:使用`interface`关键字 + +```kotlin +interface FlyingAnimal { + fun fly() +} +``` + +虽然Kotlin接口支持属性声明,然而它在Java源码中是通过一个get方法来实现的。在接口的属性并不能像Java接口那样,被直接赋值一个常量。如以下这样是错误的: + +```kotlin +interface Flyer { + val height = 1000 // error Property initializers are not allowed in interfaces + val speed: Int + // 可以支持默认实现方法,反编译可以看到是通过静态内部类来提供fly方法的默认实现的 + fun fly() { + println("I can fly") + } +} +``` + +Kotlin提供了另外一种方式来实现这种效果: + +```kotlin +interface Flyer { + val height + get() = 1000 +} +``` + + + +## 函数:通过`fun`关键字定义 + +```kotlin +fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) +} +``` +如果你没有指定它的返回值,它就会返回`Unit`与`Java`中的`void`类似,但是`Unit`是一个类型,而void只是一个关键字。`Unit`可以省略。 +你当然也可以指定任何其它的返回类型: + +```kotlin +fun maxOf(a: Int, b: Int): Int { + if (a > b) { + return a + } else { + return b + } +} +``` + +### 表达式函数体 + +然而如果返回的结果可以使用一个表达式计算出来,你可以不使用括号而是使用等号: + +```kotlin +fun add(x: Int,y: Int) : Int = x + y // 省略了{} +``` + +Kotlin支持这种单行表达式与等号的语法来定义函数,叫做表达式函数体,作为区分,普通的函数声明则可以叫做代码块函数体。如你所见,在使用表达式函数体的情况下我们可以不声明返回值类型,这进一步简化了语法。 + + + +我们可以给参数指定一个默认值使得它们变得可选,这是非常有帮助的。这里有一个例子,在`Activity`中创建了一个函数用来`Toast`一段信息: + +```kotlin +fun toast(message: String, length: Int = Toast.LENGTH_SHORT) { + Toast.makeText(this, message, length).show() +} +``` +上面代码中第二个参数`length`指定了一个默认值。这意味着你调用的时候可以传入第二个值或者不传,这样可以避免你需要的重载函数: + +```kotlin +toast("Hello") +toast("Hello", Toast.LENGTH_LONG) +``` + +### 自定义`get set`方法: + +`Kotlin`会默认创建`set get`方法,我们也可以自定义`get set`方法: +`kotlin`预留了一个在`set`和`get`中访问的变量`field`关键字: + +```kotlin +class Person constructor() { + var name: String = "" + get() = field + set(value) { + field = "$value" + } + + var age: Int = 0 + get() = field + set(value) { + field = value + } +} +``` +按照惯例`set`参数的名称是`value`,但是如果你喜欢你可以选择一个不同的名称。 + +### 可变长参数函数:使用`vararg`关键字 + +```kotlin +fun vars(vararg v:Int){ + for(vt in v){ + print(vt) + } +} + +// 测试 +fun main(args: Array) { + vars(1,2,3,4,5) // 输出12345 +} +``` + +### 命名风格 + +如果拿不准的时候,默认使用`Java`的编码规范,比如: + +- 使用驼峰法命名(并避免命名含有下划线) +- 类型名以大写字母开头 +- 方法和属性以小写字母开头 +- 使用4个空格缩进 +- 公有函数应撰写函数文档,这样这些文档才会出现在`Kotlin Doc`中 + + +### 冒号 + +类型和超类型之间的冒号前要有一个空格,而实例和类型之间的冒号前不要有空格: + +```kotlin +interface Foo : Bar { + fun foo(a: Int): T +} +``` + +### 类头格式化 + +有少数几个参数的类可以写成一行: + +```kotlin +class Person(id: Int, name: String) +``` + +具有较长类头的类应该格式化,以使每个主构造函数参数位于带有缩进的单独一行中。 此外,右括号应该另起一行。如果我们使用继承, +那么超类构造函数调用或者实现接口列表应位于与括号相同的行上: + +```kotlin +class Person( + id: Int, + name: String, + surname: String +) : Human(id, name) { + // …… +} +``` + +对于多个接口,应首先放置超类构造函数调用,然后每个接口应位于不同的行中: + +```kotlin +class Person( + id: Int, + name: String, + surname: String +) : Human(id, name), + KotlinMaker { + // …… +} +``` + +### `Unit`:让函数调用皆为表达式 + +如果函数返回`Unit`类型,该返回类型应该省略: + +```kotlin +fun foo() { // 省略了 ": Unit" + +} +``` + +之所以不能说Java中的函数调用皆是表达式,是因为存在特例void。众所周知,在Java中如果声明的函数没有返回值,那么它就需要用void来修饰,如: + +```java +void foo() { + System.out.println("return nothing") +} +``` + +所以foo()就不具有值和类型信息,它就不能算作一个表达式。在Kotlin中,函数在所有的情况下都具有返回类型,所以他们引入了Unit来替代Java中的void关键字。 + +Unit与Int一样,都是一种类型,然而它不代表任何信息,用面向对象的术语来描述就是一个单例,它的实例只有一个,可写为()。 + + + +[下一篇:Kotlin学习教程(二)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%8C).md) + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! + diff --git "a/KotlinCourse/11.Kotlin_\350\256\276\350\256\241\346\250\241\345\274\217.md" "b/KotlinCourse/11.Kotlin_\350\256\276\350\256\241\346\250\241\345\274\217.md" new file mode 100644 index 00000000..5a3a5989 --- /dev/null +++ "b/KotlinCourse/11.Kotlin_\350\256\276\350\256\241\346\250\241\345\274\217.md" @@ -0,0 +1,458 @@ +11.Kotlin_设计模式 +=== + + + +## 工厂模式 + +简单工厂的模式,它的核心作用是通过一个工厂类隐藏对象实例的创建逻辑,而不需要暴露给客户端。典型的使用场景就是当拥有一个父类与多个子类的时候,我们可以通过这种模式来创建子类对象。 + +假设现在有一个电脑加工厂,同时生产个人电脑和服务器主机。我们用熟悉的工厂模式设计描述其业务逻辑: + +```kotlin +interface Computer { + val cpu: String +} +class PC(override val cpu: String = "Core") : Computer +class Server(override val cpu: String = "Xeon") : Computer + +enum class ComputerType { + PC, Server +} + +class ComputerFactory { + fun produce(type: ComputerType): Computer { + return when (type) { + ComputerType.PC -> PC() + ComputerType.Server -> Server() + } + } +} +fun main() { + val pc = ComputerFactory().produce(ComputerType.PC) + println(pc.cpu) // Core +} +``` + +以上代码通过调用ComputerFactory类的produce方法来创建不同的Computer子类对象,这样我们就把创建实例的逻辑和客户端之间实现解耦。这是用Kotlin模仿Java中很标准的工厂模式设计,它改善了程序的可维护性,但创建对象的表达上却显得不够简洁。当我们在不同的地方创建Computer的子类对象时,我们都需要先创建一个ComputerFactory类对象。 + +### 用单例代替工厂类 + +我们已经知道Kotlin支持用object来实现Java中的单例模式。所以可以实现一个ComputerFactory单例,而不是一个工厂类: + +```kotlin +object ComputerFactory { + fun produce(type: ComputerType) : Computer { + return when (type) { + ComputerType.PC -> PC() + ComputerType.Server -> Server() + } + } +} +fun main() { + // 这样我们就不用再每次都创建对象了 + val pc = ComputerFactory.produce(ComputerType.PC) + println(pc.cpu) +} +``` + +由于我们通过传入Computer类型来创建不同的对象,所以这里的produce又显得多余。我们可以用运算符重载来通过operator操作符重载invoke方法来代替produce,从而进一步简化表达: + +```kotlin +object ComputerFactory { + operator fun invoke(type: ComputerType) : Computer { + return when (type) { + ComputerType.PC -> PC() + ComputerType.Server -> Server() + } + } +} +fun main() { + // 这样就会非常简洁 + val pc = ComputerFactory(ComputerType.PC) + println(pc.cpu) +} +``` + + + +### 伴生对象创建静态工厂方法 + +上面的工厂模式实现已经足够优雅,然而依旧不够完美: 我们是否可以直接通过Computer()而不是ComputerFactory()来创建一个实例呢? + +我们可以通过在Computer接口中定义一个伴生对象,这样就能实现以上的需求: + +```kotlin +interface Computer { + val cpu: String + companion object { + operator fun invoke(type: ComputerType) : Computer { + return when (type) { + ComputerType.PC -> PC() + ComputerType.Server -> Server() + } + } + } +} + +fun main() { + val pc = Computer(ComputerType.PC) + println(pc.cpu) +} +``` + +我们可以直接通过Computer来调用其伴生对象中的方法。当然,如果你觉得还是Factory这个名字好,那么也没有问题,我们可以用Factory来命名Computer的伴生对象,如下: + +```kotlin +interface Computer { + val cpu: String + companion object Factory { + operator fun invoke(type: ComputerType) : Computer { + return when (type) { + ComputerType.PC -> PC() + ComputerType.Server -> Server() + } + } + } +} + +fun main() { + val pc = Computer.Factory(ComputerType.PC) + println(pc.cpu) +} +``` + + + +### 扩展伴生对象方法 + +依靠伴生对象的特性,我们已经很好地实现了经典的工厂模式。同时,这种方式还有一种优势,它比原有Java中的设计更加强大。假设实际业务中我们是Computer接口的使用者,比如它是工程引入的第三方类库,所有的类的实现细节都得到了很好的隐藏。那么,如果我们希望进一步改造其中的逻辑,Kotlin中伴生对象的方式同样可以依靠其扩展函数的特性,很好的实现这一需求: + +比如我们希望给Computer增加一种功能,通过CPU型号来判断电脑类型,那么可以如下实现: + +```kotlin +fun Computer.Factory.fromCPU(cpu: String) : ComputerType? = when(cpu) { + "Core" -> ComputerType.PC + "Xeon" -> ComputerType.Server + else -> null +} +fun main() { + val pc = Computer.Factory.fromCPU("Core") + println(pc) +} +``` + + + +### 内联函数简化抽象工厂 + +Kotlin中的内联函数有一个很大的作用,就是可以具体化参数类型。利用这一特性,可以改进一种更复杂的工厂模式,称为抽象工厂。 上面的例子中已经用工厂模式很好的处理了一个产品登记结构的问题。但是如果现在引入了品牌商的概念,我们有好几个不同的电脑品牌,比如Dell、Asus、Acer,那么就有必要再增加一个工厂类。然而,我们并不希望对每个模型都建立一个工厂,这会让代码变得难以维护,所以这时候我们就需要引入抽象工厂模式。 + + + +##### 抽象工厂模式 + +为创建一组相关或相互依赖的对象提供一个接口,而且无须指定他们的具体类。 + +```kotlin +interface Computer +class Dell: Computer +class Asus: Computer +class Acer: Computer + +class DellFactory: AbstractFactory() { + override fun produce() = Dell() +} +class AsusFactory: AbstractFactory() { + override fun produce() = Asus() +} +class AcerFactory: AbstractFactory() { + override fun produce() = Acer() +} + +abstract class AbstractFactory { + abstract fun produce(): Computer + companion object { + operator fun invoke(factory: AbstractFactory): AbstractFactory { + return factory + } + } +} +fun main(args: Array) { + val dellFactory = AbstractFactory(DellFactory()) + val dell = dellFactory.produce() + println(dell) +} +``` + +可以看出,每个电脑品牌拥有一个代表电脑产品的类,它们都实现了Computer接口。此外每个品牌也还有一个用于生产电脑的AbstractFactory子类,可通过AbstractFactory类的伴生对象中的invoke方法,来构造具体品牌的工厂类对象。 + +由于Kotlin语法的简洁,以上例子的抽象工厂类的设计也比较直观。然而,当你每次创建具体的工厂类时,都需要传入一个具体的工厂类对象作为参数进行构造,这个在语法上显然不够优雅。下面我们就来看看,如何用Kotlin中的内联函数来改善这一情况。我们所需要做的,就是去重新实现AbstractFactory类中的invoke方法。 + +```kotlin +abstract class AbstractFactory { + abstract fun produce(): Computer + companion object { + // 增加reified关键字 + inline operator fun invoke(): AbstractFactory = + when (T::class) { + Dell::class -> DellFactory() + Asus::class -> AsusFactory() + Acer::class -> AcerFactory() + else -> throw IllegalArgumentException() + } + } +} +``` + +这下我们的invoke方法定义的前缀变长了很多,但是不要害怕,如果你已经掌握了内联函数的具体应用,应该会很容易理解它。我们来分析下这段代码: + +- 通过将invoke方法用inline定义为内联函数,我们就可以引入reified关键字,使用具体化参数类型的语法特性。 +- 要具体化的参数类型为Computer,在invoke方法中我们通过判断它的具体类型,来返回对应的工厂类对象。 + +再来看看通过上面内联函数改善后的工厂类的创建语法表达: + +```kotlin +fun main(args: Array) { + val dellFactory = AbstractFactory() + val dell = dellFactory.produce() + println(dell) +} +``` + +现在终于可以用类似创建一个泛型类对象的方式,来构建一个抽象工厂具体对象了。 + + + +## 构造者模式 + +构造者模式与单例模式一样,它主要做的事情就是将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。 + +工厂模式和构造函数都存在相同的问题,就是不能很好地扩展到大量的可选参数。假设我们现在有个机器人类,它含有多个属性:代号、名字、电池、重量、高度、速度、音量等。很多产品都不具有其中的某些属性,比如不能走、不能发声,甚至有的机器人也不需要电池。 + +一种糟糕的做法就是设计一个一开头你所看到Robot类,把所有的属性都作为构造函数的参数。或者,你也可能采用过重叠构造器模式,即先提供一个只有必要参数的构造函数,然后再提供其他更多的构造函数,分别具有不同情况的可选属性。虽然这种模式在调用的时候改进不少,但同样存在明显的确定,因为随着构造函数的参数数量增加,很快我们就会失去控制,代码变得难以维护。 + +构建者模式可以避免以上问题,我们用Kotlin来实现Java中的构建者模式: + +```kotlin +class Robot private constructor ( + val code: String, + val battery: String?, + val height: Int?, + val weight: Int?) { + + class Builder(val code: String) { + private var battery: String? = null + private var height: Int? = null + private var weight: Int? = null + + fun setBattery(battery: String?): Builder { + this.battery = battery + return this + } + + fun setHeight(height: Int): Builder { + this.height = height + return this + } + + fun setWeight(weight: Int): Builder { + this.weight = weight + return this + } + fun build(): Robot { + return Robot(code, battery, height, weight) + } + } + } +} + +var robot = Robot.Builder("007") + .setBattery("R6") + .setHeight(100) + .setWeight(80) + .build() +``` + +为了避免代码太长,上面的例子中只选择了4个属性,其中code是必须属性,battery、height、weight为可选属性。我们来分析一下它的具体思路: + +- Robot类内部定义了一个嵌套类Builder,由它负责创建Robot对象 +- Robot类的构造函数用private进行修饰,这样可以确保使用者无法直接通过Robot声明实例 +- 通过在Builder类中定义set方法来对可选的属性进行设置 +- 最终调用Builder类中的build方法来返回一个Robot对象 + +这种链式调用的设计看起来确实优雅了很多,同时对于可选参数的设置也显得比较语义化。此外,构建者模式另外一个好处就是解决了多个可选参数的问题,当我们创建对象实例时,只需要用set方法对需要的参数进行赋值即可。 + +然而,构建者模式也存在一些不足: + +- 如果业务需求的参数很多,代码依然会显得比较长 +- 你可能会在使用Builder的时候忘记在最后调用build方法 +- 由于在创建对象的时候,必须先创建它的构造器,因此额外增加了多余的开销,在某些十分注重性能的情况下,可能就存在一定的问题。 + +事实上,当用Kotlin设计程序时,我们可以在绝大多数情况下避免使用构建者模式。《Effective Java》在介绍构建者模式时,是这样子描述它的:本质上builder模式模拟了具名的可选参数。幸运的是,Kotlin也是这样一门拥有具名可选参数的编程语言。 + +#### 具名的可选参数 + +Kotlin中的函数和构造器都支持这一特性,它主要表现为两点: + +- 在具体化一个参数的取值时,可以通过带上它的参数名,而不是它在所有参数中的位置决定。 +- 由于参数可以设置默认值,这允许我们只给出部分参数的取值,而不必是所有的参数。 + +因此,我们可以直接使用Kotlin中原生的语法特性来实现构建者模式的效果。现在重新设计以上的Robot例子: + +```kotlin +class Robot( + val code: String, + val battery: String? = null, + val height: Int? = null, + val weight: Int? = null +) + + +private fun main() { + val robot1 = Robot(code = "007") + val robot2 = Robot(code = "007", battery = "R6") + val robot3 = Robot(code = "007", height = 100, weight = 80) + + println(robot1) +} +``` + +可以发现,相比构建者模式,通过具名的可选参数构造类具有很多优点: + +- 代码变得十分简单,这不仅表现在Robot类的结构体代码量,我们在声明Robot对象时的语法也要更加简洁 +- 声明对象时,每个参数名都可以是显式的,并且无须按照顺序书写,非常方便灵活 +- 由于Robot类的每个对象都是val声明的,相较构建者模式中的var的方案更加安全,这在要求多线程并发安全的业务场景中会显得更有优势。 + +此外,如果你的类的功能足够简单,更好的思路是用data class直接声明一个数据类。数据类同样支持以上的所有特性。 + + + +#### require方法对参数进行约束 + +我们再来看看构建者模式的另外一个作用,就是可以在build方法中对参数添加约束条件。举个例子,假设一个机器人的重量必须根据电池的型号决定,那么在未传入电池型号之前,你便不能对weight属性进行赋值,否则就会抛出异常。现在重新修改一下上面build方法的实现: + +```kotlin +fun build(): Robot { + if (weight != null && battery == null) { + throw IllegalArgumentException("Battery should be determined when setting weight.") + } else { + return Robot(code, battery, height, weight) + } +} +``` + +这种在build方法中对参数进行约束的手段,可以让业务变得更加安全。那么,通过具名的可选参数来构造类的方案该如何实现呢? + +显然,我们同样可以在Robot类的init方法中增加以上的校检代码。然而在Kotlin中,我们在类或函数中还可以使用require关键字进行参数限制,本质上它是一个内联的方法,有点类似于Java的assert。 + +```kotlin +class Robot( + val code: String, + val battery: String? = null, + val height: Int? = null, + val weight: Int? = null +) { + init { + require(weight == null || battery != null) { + "Battery should be determined when setting weight." + } + } +} +``` + +可见,Kotlin的require方法可以让我们的参数约束代码在语义上变得更加友好。总的来说,在Kotlin中我们应该尽量避免使用构建者模式,因为Kotlin支持具名的可选参数,这让我们可以在构造一个具有多个可选参数类的场景中,设计出更加简洁并利于维护的代码。 + + + + + +## 观察者模式 + +观察者模式定义了一个一对多的依赖关系,让一个或多个观察者对象监听一个主题对象。这样一来,当被观察者状态发生改变时,需要通知相应的观察者,使这些观察者对象能够自动更新。 + +简单来说,观察者模式无非做两件事情: + +- 订阅者(observer)添加或删除对发布者(publisher)的状态监听。 +- 发布者状态改变时,将事件通知给监听它的所有观察者,然后观察者执行响应逻辑。 + +Java自身的标准库提供了java.util.Observable类和java.util.Observer接口,来帮助实现观察者模式,接下来我们就采用它们来实现一个动态更新股价的例子。 + +```kotlin +class StockUpdate: Observable() { + val observers = mutableSetOf() + fun setStockChanged(price: Int) { + this.observers.forEach { it.update(this, price)} + } +} +class StockDisplay: Observer { + override fun update(o: Observable, price: Any) { + if (o is StockUpdate) { + println("The latest stock price is ${price}") + } + } +} +fun main(args: Array) { + val su = StockUpdate() + val sd = StockDisplay() + su.observers.add(sd) + su.setStockChanged(100) +} +``` + +上面是通过Kotlin使用Java标准库中的类和方法来实现了观察者模式。事实上,Kotlin的标准库额外引入了可被观察的委托属性,也可以利用它来实现同样的场景。 + +我们可以先用这一委托属性来改造以上的程序: + +```kotlin +import kotlin.properties.Delegates + +interface StockUpdateListener { + fun onRise(price: Int) + fun onFall(price: Int) +} + +class StockDisplay: StockUpdateListener { + override fun onRise(price: Int) { + println("The latest stock price has risen to ${price}") + } + override fun onFall(price: Int) { + println("The latest stock price has fell to ${price}") + } +} + +class StockUpdate { + var listeners = mutableSetOf() + var price: Int by Delegates.observable(0) {_, old, new -> + listeners.forEach { + if (new > old) it.onRise(price) else it.onFall(price) + } + } +} +fun main(args: Array) { + val su = StockUpdate() + val sd = StockDisplay() + su.listeners.add(sd) + su.price = 100 + su.price = 98 +} +// 执行结果 +The latest stock price has risen to 100 +The latest stock price has fell to 98 +``` + +如果你仔细思考,会发现实现java.util.Observer接口的类只能覆写update方法来编写响应逻辑,也就是说如果存在多种不同的逻辑响应,我们也必须通过在该方法中进行区分实现,显然这会让订阅者的代码显得臃肿。换个角度,如果我们把发布者的事件推送看成一个第三方服务,那么它提供的API接口只有一个,API调用者必须承担更多的职责。 + +显然,使用Delegates.observable()的方案更加灵活。它提供了三个参数,依次代表委托属性的元数据KProperty对象、旧值以及新值。通过额外定义一个StockUpdateListener接口,我们可以把上涨和下跌的不同响应逻辑封装成接口方法,从而在StockDisplay中实现该接口的onRise和onFall方法,实现了解耦。 + + + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/KotlinCourse/2.Kotlin_\351\253\230\351\230\266\345\207\275\346\225\260&Lambda&\345\206\205\350\201\224\345\207\275\346\225\260.md" "b/KotlinCourse/2.Kotlin_\351\253\230\351\230\266\345\207\275\346\225\260&Lambda&\345\206\205\350\201\224\345\207\275\346\225\260.md" new file mode 100644 index 00000000..23458c79 --- /dev/null +++ "b/KotlinCourse/2.Kotlin_\351\253\230\351\230\266\345\207\275\346\225\260&Lambda&\345\206\205\350\201\224\345\207\275\346\225\260.md" @@ -0,0 +1,742 @@ +Kotlin之Lambda&内联函数(七) +=== + + + +函数式语言一个典型的特征就在于函数是头等公民-我们不仅可以像类一样在顶层直接定义一个函数,也可以在一个函数内部定义一个函数,例如: + +```kotlin +fun foo(x: Int) { + fun double(y: Int): Int { + return y * 2 + } + println(double(x)) +} + +执行foo(1)结果为2 +``` + + + +## 高阶函数 + +Kotlin天然支持了部分函数式特性。函数式语言一个典型的特征就在于函数是头等公民——我们不仅可以像类一样在底层直接定义一个函数,也可以在一个函数内部定义一个局部函数。 + +```kotlin +fun foo(x: Int) { + fun double(y: Int): Int { + return y * 2 + } + println(double(x)) +} +``` + +### 抽象和高阶函数 + +我们会善于对熟悉 或重复的事物进行抽象,比如2岁左右的小孩就会开始认识数字1、2、3....之后,我们总结除了一些公共的行为,如对数字做加减、求立方,这被称为过程,它接收的数字是一种数据,然后也可能产生另一种数据。 + +过程也是一种抽象,几乎我们所熟悉的所有高级语言都包含了定义过程的能力,也就是函数。 + +然而,在我们以往熟悉的编程中,过程限制为只能接收数据为参数,这个无疑限制了进一步抽象的能力。 + +由于我们经常会遇到一些同样的程序设计模式能够用于不同的过程,比如一个包含了正整数的列表,需要对它的元素进行各种转换操作,例如对所有元素都乘以3,或者都除以2。我们就需要提供一种模式,同时接收这个列表及不同的元素操作过程,最终返回一个新的列表。 + +为了把这种类似的模式描述为相应的概念,我们就需要构造出一种更加高级的过程,表现为:接收一个或多个过程为参数,或者以一个过程作为返回结果。这个就是所谓的高阶函数,你可以把它理解为“以其他函数作为参数或返回值的函数”。高阶函数是一种更加高级的抽象机制,它极大地增强了语言的表达能力。 + + + +### 实例: 函数作为参数的需求 + +Shaw因为旅游喜欢上了地理,然后他建了一个所有国家的数据库。作为一名程序员,他设计了一个CountryApp类对国家数据进行操作。Shaw偏好欧洲的国家,于是他设计了一个程序来获取欧洲的所有国家。 + +```kotlin +data class Country { + val name: String, + val continient: String, + val population: Int +} + +class CountryApp { + fun filterCountries(countries: List): List { + val res = mutableListOf() + for (c in countries) { + if (c.continent == "EU") { // EU代表欧洲 + res.add(c) + } + } + return res + } +} +``` + +后来Shaw对非洲也产生了兴趣,于是他又改进了上述方法的实现,支持根据具体的州来筛选国家。 + +```kotlin +fun filterCountries(countries: List, continient: String): List { + val res = mutableListOf() + for (c in countries) { + if (c.continient == continient) { + res.add(c) + } + } + return res +} +``` + +以上程序具备了一定的复用性。然而,Shaw的地理知识越来越丰富了,他想对国家的特点做进一步的研究,比如筛选具有一定人口规模的国家,于是代码又变成下面这个样子: + +```kotlin +fun filterCountries(countries: List, continient: String, population: Int) : List { + val res = mutableListOf() + for (c in countries) { + if (c.continient == continient && c.population > population) { + res.add(c) + } + } + return res +} +``` + +新增了一个population的参数来代表人口(单位:万)。Shaw开始感觉到不对劲,如果按照现有的设计,更多的筛选条件会作为方法参数而不断增加,而且业务逻辑也会高度耦合。 + +解决问题的核心在于对filterCountries方法进行解耦,我们能否把所有的筛选逻辑行为都抽象成一个参数呢?传入一个类对象是一种解决方法,我们可以根据不同的筛选需求创建不同的子类,它们都各自实现了一个校检方法。然而,Shaw了解到Kotlin是支持高阶函数的,理论上我们同样可以把筛选的逻辑变成一个方法来传入,这样思路更简单。 + +他想要进一步了解高级的特性,所以很快写了一个新的测试类: + +```kotlin +class CountryTest { + fun isBigEuropeanCountry(country: Country): Boolean { + return country.continient == "EU" && country.population > 10000 + } +} +``` + +调用isBigEuropeanCountry方法就能够判断一个国家是否是一个人口超过1亿的欧洲国家。然而,怎样才能把这个方法变成filterCountries方法的一个参数呢?要实现这一点似乎要先解决以下两个问题: + +- 方法作为参数传入,必须像其他参数一样具备具体的类型信息 + + 在kotlin中,函数类型的格式非常简单: + + - 通过 -> 符号来组织参数类型和返回值类型,左边是参数类型,右边是返回值类型 + - 必须用一个括号来包裹参数类型,如果是一个没有参数的函数类型,参数类型部分就用()表示 + - 返回值类型即使是Unit,也必须显式声明 + + 举个例子: + + ```kotlin + (Int) -> Unit + () -> Unit + (Int, String) -> Unit + // 还支持为声明参数指定名字 + (errCode: Int, errMsg: String) -> Unit + // ?表示可选,可在某种情况下为空 + ((errCode: Int, errMsg: String?) -> Unit)? + ``` + + 在学习了Kotlin函数类型知识之后,Shaw重新定义了filterCountries方法的参数声明: + + ```kotlin + // 增加了一个函数类型的参数test + fun filterCountries(countries: List, test: (Country) -> Boolean): List { + val res = mutableListOf() + for (c in countries) { + // 直接调用test函数来进行筛选 + if (test(c)) { + res.add(c) + } + } + return res + } + ``` + + 接下来就是如何把isBigEuropeanCountry方法传递给filterCountries呢? 直接把isBigEuropeanCountry当参数肯定不行,因为函数名并不是一个表达式不具有类型信息。所以我们需要的是一个单纯的方法引用表达式。 + +- 需要把isBigEuropeanCountry的方法引用当做参数传递给filterCountries + + Kotlin存在一种特殊的语法,通过两个冒号来实现对于某个类的方法进行引用(方法引用表达式)。以上面的代码为例,假如我们有一个CountryTest类的对象实例countryTest,如果要引用它的isBigEuropeanCountry方法,就可以这样写: + + ```kotlin + countryTest::isBigEuropeanCountry + ``` + + 于是,Shaw便使用了方法引用来传递参数: + + ```kotlin + val countryApp = CountryApp() + val countryTest = CountryTest() + val countries = ... + + countryApp.filterContries(countries, countryTest::isBigEuropeanCountry) + ``` + +经过重构后的程序显然比之前要优雅许多,程序可以根据任意的筛选需求,调用同一个filterCountries方法来获取国家数据。 + +#### 方法引用表达式更多使用场景 + +此外,我们还可以直接通过这种语法,来定义一个类的构造方法引用变量。 + +```kotlin +class Book(val name: String) { + fun main(args: Array) { + val getBook = ::Book + println(getBook("Dive into Kotlin").name) + } +} +``` + +可以发现,getBook类型为(name: String) -> Book。类似的道理,如果我们要引用某个类的成员变量,如Book类中的name,就可以这样引用: + +```kotlin +Book::name +``` + +以上创建的Book::name的类型为(Book) -> String。 + + + +## 匿名函数 + +再来思考下上面代码中的CountryTest类,这仍算不上是一种很好的方案。因为每增加一个需求,我们都需要在类中专门写一个新增的筛选方法。然而Shaw的需求很多都是临时性的,不需要被复用。Shaw觉得这样还是比较麻烦,他打算用匿名函数对程序进一步的优化。 + +Kotlin支持在缺省函数名的情况下,直接定义一个函数。所以isBigEuropeanCountry方法我们可以直接定义为: + +```kotlin +// 没有函数名字 +fun(country: Country): Boolean { + return country.continient == "EU" && country.population > 10000 +} +``` + +于是,Shaw直接调用filterCountries,如下: + +```kotlin +countryApp.filterCountries(countries, fun(country: Country): Boolean) { + return country.continient == "EU" && country.population > 10000 +}) +``` + +这一次我们甚至不需要CountryTest这个类了,代码的简洁性又上了一层楼。Shaw开始意识到Kotlin这门语言的魅力,很快他发现还有一种语法可以让代码更简单,这就是Lambda表达式。 + +我们继续看上面的filterCountries方法的匿名函数,会发现: + +- fun(country: Country)显得比较啰嗦,因为编译器会推导类型,所以只需要一个代表变量的country就行了。 +- return关键字也可以省略,这里返回的是一个有值的表达式 +- 模仿函数类型的语法,我们可以用 -> 把函数和返回值连接在一起 + +因此,简化后的表达就变成了这个样子: + +```kotlin +countryApp.filterCountries(countries, { + country -> + country.continient == "EU" && country.population > 10000 +}) +``` + +这就是Lambda表达式,它与匿名函数一样,是一种函数字面量。 + +Lambda的语法: + +- 一个Lambda表达式必须通过{}来包裹 +- 如果Lambda声明了参数部分的类型,且返回值类型支持类型推导,那么Lambda变量就可以省略函数类型声明 +- 如果Lambda变量声明了函数类型,那么Lambda的参数部分的类型就可以省略 + +此外,如果Lambda表达式返回的不是Unit,那么默认最后一行表达式的值类型就是返回值类型,如: + +```kotlin +val foo = { x: Int -> + val y = x + 1 + y // 返回值是y +} +``` + +## Lambda表达式 + + +> “Lambda 表达式”(lambda expression)其实就是匿名函数,`Lambda`表达式基于数学中的`λ`演算得名,直接对应于其中的`lambda`抽象 +> `(lambda abstraction)`,是一个匿名函数,即没有函数名的函数。`Lambda`表达式可以表示闭包(注意和数学传统意义上的不同)。 + +`Java 8`的一个大亮点是引入`Lambda`表达式,使用它设计的代码会更加简洁。 + +```java +// 没有使用Lambda的老方法: +button.addActionListener(new ActionListener(){ + public void actionPerformed(ActionEvent ae){ + System.out.println("Actiondetected"); + } +}); +// 使用Lambda: +button.addActionListener(()->{ + System.out.println("Actiondetected"); +}); + + +// 不采用Lambda的老方法: +Runnable runnable1=new Runnable(){ + @Override + public void run(){ + System.out.println("RunningwithoutLambda"); + } +}; +// 使用Lambda: +Runnable runnable2=()->{ + System.out.println("RunningfromLambda"); +}; +``` + +`Lambda`能让代码更简洁,Kotlin的支持如下: + +- `lambda`表达式总是被大括号括着 +- 其参数(如果有的话)在`->`之前声明(参数类型可以省略), +- 函数体(如果存在的话)在`->`后面。 + +`Lambda`表达式是定义匿名函数的简单方法。由于`Lambda`表达式避免在抽象类或接口中编写明确的函数声明,进而也避免了类的实现部分, +所以它是非常有用的。在`Kotlin`语言中,可以将一函数作为另一函数的参数。 + +`Lambda`表达式由箭头左侧函数的参数(在圆括号里的内容)定义的,将值返回到箭头右侧。 +`view.setOnClickListener({ view -> toast("Click")})` +在定义函数时,必须在箭头的左侧用方括号,并指定参数值,而函数的执行代码在箭头右侧。如果左侧不使用参数,甚至可以省去左侧部分: +`view.setOnClickListener({ toast("Click") })` +如果函数的最后一个参数是一个函数的话,可以将作为参数的函数移到圆括号外面: +`view.setOnClickListener() { toast("Click") }` + + +先看一个例子: + +```kotlin +fun compare(a: String, b: String): Boolean { + return a.length < b.length +} +max(strings, compare) +``` +就是找出`strings`里面最长的那个。但是我个人觉得`compare`还是很碍眼的,因为我并不想在后面引用他,那我怎么办呢,就是用“匿名函数”方式。 +```kotlin +max(strings, (a,b)->{a.length < b.length}) +``` + +`(a,b)->{a.length < b.length}`就是一个没有名字的函数,直接作为参数赋给`max`方法的第二个参数。但这个方法有很多东西都没有写明,如: + +- 参数的类型 +- 返回值的类型 + +但这些真的必要吗?`a.length < b.length`很明显返回一个`Boolean`的值,再就是`max`的定义中肯定也定义了这个函数的参数类型和返回值类型。 +这么明显的事为什么不让计算机自己去做而要让人写代码去做呢?这就是匿名函数的好处了。到这里,我们已经和`Lambda`很接近了。 + +```kotlin +val sum: (Int, Int) -> Int = { x, y -> x + y } +``` + +`Lambda`表达式就是被大括号括着的那一部分,在`->`符号之前有参数声明,函数体跟在一个`->`符号之后。 +而且此`Lambda`表达式之前有一个匿名的函数声明(在此例中两个`Int`型的输入,一个`Int`型的返回值),这个声明是可以不使用的。 +则此`Lambda`表达式变成`val sum = { x: Int, y: Int -> x + y }`,此时`Lambda`表达式会根据主体中的最后一个(或可能是单个)表达式会视为 +返回值。当然,在某些特定情况下,`x`、`y`的类型了是可以推断的,所以`val sum = { x, y -> x + y }`。 + +## Lambda开销 + +```kotlin +fun foo(int: Int) = { + print(int) +} +listOf(1, 2, 3).forEach { foo(it) } // 对一个整数列表的元素遍历调用foo +``` + +这里,你可定会纳闷it是啥?其实它也是Kotlin简化Lambda表达的一种语法糖,叫做单个参数的隐式名称,代表了这个Lambda所接收的单个参数。这里的调用等价于: + +```kotlin +listOf(1, 2, 3).forEach { item -> foo(item) } +``` + +默认情况下,我们可以直接用it来代表item,而不需要用item -> 进行声明。 + +我们看一下foo函数用IDE转换后的Java代码: + +```java +@JvmStatic +@NotNull +public static final Function0 foo(final int var0) { + return (Function0)(new Function0() { + // $FF: synthetic method + // $FF: bridge method + public Ojbect invoke() { + this.invoke(); + return Unit.INSTANCE; + } + public final void invoke() { + int var1 = var0; + System.out.printlln(var1); + } + }); +} +``` + +以上是字节码反编译的Java代码,从中我们可以发现Kotlin实现Lambda表达式的机理。 + +### Function类型 + +Kotlin在JVM层设计了Function类型(Function0、Function1 ... Function22、FunctionN)来兼容Java的Lambda表达式,其中的后缀数字代表了Lambda参数的数量,如以上的foo函数构建的其实是一个无参Lambda,所以对应的接口是Function0,如果有一个参数那么对应的就是Function1.它在源码是如下定义的: + +```kotlin +package kotlin.jvm.functions +interface Function1 : kotlin.Function { + fun invoke(p1: P1) : R +} +``` + +可见每个Function类型都有一个invoke方法。设计Function类型的主要目的之一就是要兼容Java,实现在Kotlin中也能调用Java的Lambda。在Java中,实际上并不支持把函数作为参数,而是通过函数式接口来实现这一特性。 + +foo函数的返回类型是Function()。这也意味着,如果我们调用了foo(n),那么实质上仅仅是构造了一个Function()对象。这个对象并不等价于我们要调用的过程本身。通过源码可以发现,需要调用Function()的invoke方法才能执行println方法。所以上面的例子必须如下修改,才能最终打印出我们想要的结果: + +```kotlin +fun foo(int: Int) = { + print(int) +} +listOf(1, 2, 3).forEach { foo(it).invoke() } // 增加了invoke调用 +``` + + + +但是invoke这种语法显得丑陋,不符合Kotlin简洁表达的设计理念,所以我们还可以用熟悉的括号调用来替代invoke,如下所示: + +```kotlin +listOf(1, 2, 3).forEach{ foo(it)() } +``` + +#### 闭包 + +在Kotlin中,你会发现匿名函数体、Lambda在语法上都存在“{}",由这对花括号包裹的代码如果访问了外部环境变量则被称为一个闭包。 + +一个闭包可以被当做参数传递或直接使用,它可以简单的看成”访问外部环境变量的函数“。Lambda是Kotlin中最常见的闭包形式。 + +与Java不一样的地方在于,Kotlin中的闭包不仅可以访问外部变量,还能够对其进行修改,如下: + +```kotlin +var sum = 0 +listOf(1, 2, 3).filter { it > 0 }.forEach { + sum += it +} +println(sum) // 6 +``` + +## 内联函数 + +在我开始学的时候,一直没搞明白内联函数到底是干什么? 有什么作用?Kotlin中的内联函数其实显得有点尴尬,因为它之所以被设计出来,主要是为了优化Kotlin支持Lambda表达式之后所带来的开销。然而,在Java中我们似乎并不需要特别关注这个问题,因为在Java 7之后,JVM引入了一种叫做invokedynamic的技术,它会自动帮助我们做Lambda优化。但是为什么Kotlin要引入内联函数这种手动的语法呢? 这主要还是因为Kotlin要兼容Java 6。 + + + +## 优化Lambda开销 + +在Kotlin中每声明一个Lambda表达式,就会在字节码中产生一个匿名类。该匿名类包含了一个invoke方法,作为Lambda的调用方法,每次调用的时候,还会创建一个新的对象。可想而知,Lambda语法虽然简洁,但是额外增加的开销也不少。并且,如果Lambda捕捉了某个变量,那么每次调用的时候都会创建一个新的对象,这样导致效率较低。尤其对Kotlin这门语言来说,它当今优先要实现的目标,就是在Android这个平台上提供良好的语言特性支持。Kotlin要在Android中引入Lambda语法,必须采用某种方法来优化Lambda带来的额外开销,也就是内联函数。 + +#### 1. invokedynamic + +在讲述内联函数具体的语法之前,我们先来看看Java中是如何解决这个问题的。与Kotlin这种在编译期通过硬编码生成Lambda转换类的机制不同,Java在SE 7之后通过invokedynamic技术实现了在运行期才产生相应的翻译代码。在invokedynamic被首次调用的时候,就会触发产生一个匿名类来替换中间码invokedynamic,后续的调用会直接采用这个匿名类的代码。这种做法的好处主要体现在: + +- 由于具体的转换实现是在运行时产生的,在字节码中能看到的只有一个固定的invokedynamic,所以需要静态生成的类的个数及字节码大小都显著减少。 +- 与编译时写死在字节码中的策略不同,利用invokedynamic可以把实际的翻译策略隐藏在JDK库的实现, 这极大提高了灵活性,在确保向后兼容性的同时,后期可以继续对编译策略不断优化升级 +- JVM天然支持了针对该方式的Lambda表达式的翻译和优化,这也意味着开发者在书写Lambda表达式的同时,可以完全不用关心这个问题,这极大地提升了开发的体验。 + + + +#### 2. 内联函数 + +invokedynamic固然不错,但Kotlin不支持它的理由似乎也很充分,我们有足够的理由相信,其最大的原因是Kotlin在一开始就需要兼容Android最主流的Java版本SE 6,这导致它无法通过invovkedynamic来解决Android平台的Lambda开销问题。 + +因此,作为另一种主流的解决方案,Kotlin拥抱了内联函数,在C++、C#等语言中也支持这种特性。简单的来说,我们可以用inline关键字来修饰函数,这些函数就称为了内联函数。他们的函数体在编译期被嵌入每一个被调用的地方,以减少额外生成的匿名类数,以及函数执行的时间开销。 + +所以如果你想在用Kotlin开发时获得尽可能良好的性能支持,以及控制匿名类的生成数量,就有必要来学习下内联函数的相关语法。 + +这里通过一个实际的例子,看看Kotlin的内联函数是具体如何操作的: + +```kotlin +fun main(args: Array) { + foo { + println("dive into Kotlin...") + } +} + +fun foo(block: () -> Unit) { + println("before block") + block() + println("end block") +} +``` + +首先,我们声明了一个高阶函数foo,可以接受一个类型为() -> Unit的Lambda,然后在main函数中调用它。以下是通过字节码反编译的相关Java代码: + +```java +public static final void main(@NotNull String[] args) { + Intrinsics.checkParameterIsNotNull(args, "args"); + foo((Function0)null.INSTANCE); +} + +public static final void foo(@NotNull Function0 block) { + Intrinsics.checkParameterIsNotNull(block, "block"); + String var1 = "before block"; + System.out.println(var1); + block.invoke(); + var1 = "end block"; + System.out.println(var1); +} +``` + +据我们所知,调用foo就会产生一个Function()类型的block类,然后通过invovke方法来执行,这会增加额外的生成类和调用开销。现在,我们给foo函数加上inline修饰符,如下: + +```kotlin +inline fun foo(block: () -> Unit) { + println("before block") + block() + println("end block") +} +``` + +再来看看相应的Java代码: + +```java +public static final void main(@NotNull String[] args) { + Intrinsics.checkParameterIsNotNull(args, "args"); + String va1 = "before block"; + System.out.println(var1); + // block函数体在这里开始粘贴 + String var2 = "dive into Kotlin..."; + System.out.println(var2); + // block函数体在这里结束粘贴 + var1 = "end block"; + System.out.println(var1); +} + +public static final void foo(@NotNull Function0 block) { + Intrinsics.checkParameterIsNotNull(block, "block"); + String var2 = "before block"; + System.out.println(var2); + block.invoke(); + var2 = "end block"; + System.out.println(var2); +} +``` + +果然,foo函数体代码及被调用的Lambda代码都粘贴到了相应调用的位置。试想下,如果这是一个工程中公共的方法,或者被嵌套在一个循环调用的逻辑体中,这个方法势必会被调用很多次。通过inline的语法,我们可以彻底消除这种额外调用,从而节省了开销。 + +内联函数典型的一个应用场景就是Kotlin的集合类。如果你看过Kotlin的集合类API文档或者源码实现就会发现,集合函数式API,如map、filter都被定义成内联函数,如: + +```kotlin +inline fun Array.map { + transform: (T) -> R +}: List + +inline fun Array.filter { + predicate: (T) -> Boolean +}: List +``` + +这个很容易理解,由于这些方法都接收Lambda作为参数,同时都需要对集合元素进行遍历操作,所以把相应的实现进行内联无疑是非常适合的。 + +但是内联函数不是万能的,以下情况我们应避免使用内联函数: + +- 由于JVM对普通的函数已经能够根据实际情况智能地判断是否进行内联优化,所以我们并不需要对其使用Kotlin的inline语法,那只会让字节码变得更加复杂。 +- 尽量避免对具有大量函数体的函数进行内联,这样会导致过多的字节码数量。 +- 一旦一个函数被定义为内联函数,便不能获取闭包类的私有成员,除非你把他们声明为internal。 + + + +#### noinline: 避免参数被内联 + +通过上面的例子我们已经知道,如果在一个函数的开头加上inline修饰符,那么它的函数体及Lambda参数都会被内联。然而现实中的情况比较复杂,有一种可能是函数需要接受多个参数,但我们只想对其中部分Lambda参数内联,其他的则不内联,这个又该如何处理? + +解决这个问题也很简单,Kotlin在引入inline的同时,也新增了noinline关键字,我们可以把它加在不想要被内联的参数开头,该参数便不会具有内联的效果: + +```kotlin +fun main(args: Array) { + foo ( { + println("I am inlined...") + }, { + println("I am not inlined...") + }) +} + +inline fun foo(block1: () -> Unit, noinline block2: () -> Unit) { + println("before block") + block1() + block2() + println("end block") +} + +``` + +同样的方法,再来看看反编译的Java版本: + +```java +public static final void main(@NotNull String[] args) { + Intrinsics.checkParameterIsNotNull(args, "args"); + Function0 block2$iv = (Function0)null.INSTANCE; + String var2 = "before block"; + System.out.println(var2); + // block1 被内联了 + String var3 = "I am inlined..."; + System.out.println(var3); + // block2 还是原样 + block2$iv.invoke(); + System.out.println(var2); +} +public static final void foo(@NotNull Function0 block1, @NotNull Function0 block2) { + Intrinsics.checkParameterIsNotNull(block1, "block1"); + Intrinsics.checkParameterIsNotNull(block2, "block2"); + String var3 = "before block"; + System.out.println(var3); + block1.invoke(); + block2.invoke(); + var3 = "end block"; + System.out.println(var3); +} +``` + +可以看出,foo函数的block2参数在带上noinline之后,反编译后的Java代码中并没有将其函数体代码在调用处进行替换。 + +#### 非局部返回 + +Kotlin中的内联函数除了优化Lambda开销之外,还带来了其他方面的特效,典型的就是非局部返回和具体化参数类型。我们先来看下Kotlin如何支持非局部返回。 + +以下是我们常见的局部返回的例子: + +```kotlin +fun main(args: Array) { + foo() +} +fun localReturn() { + return +} +fun foo() { + println("before local return") + localReturn() + println("after local return") + return +} +// 运行结果 +before local return +after local return +``` + +正如我们所熟知的,localReturn执行后,其函数体中的return只会在该函数的局部生效,所以localReturn()之后的println函数依旧生效。我们再把这个函数换成Lambda表达式的版本: + +```kotlin +fun main(args: Array) { + foo { return } +} +fun foo(returning: () -> Unit) { + println("before local return") + returning() + println("after local return") + return +} +// 运行结果 +Error:(2, 11)Kotlin: 'return' is not allowed here +``` + +这时,编译器报错了,就是说在Kotlin中,正常情况下Lambda表达式不允许存在return关键字。这时候,内联函数又可以排上用场了。我们把foo进行内联后再试试看: + +```kotlin +fun main(args: Array) { + foo { return } +} +inline fun foo(returning: () -> Unit) { + println("before local return") + returning() + println("after local return") + return +} +// 运行结果 +before local return +``` + +编译顺利通过了,但结果与我们的局部返回效果不同,Lambda的return执行后直接让foo函数退出了执行。如果你仔细考虑一下,可能很快就想出了原因。因为内联函数foo的函数体及参数Lambda会直接替代具体的调用。所以实际产生的代码中,retrurn相当于是直接暴露在main函数中,所以returning()之后的代码自然不会执行,这个就是所谓的非局部返回。 + +#### 使用标签实现Lambda非局部返回 + +另外一种等效的方式,是通过标签利用@符号来实现Lambda非局部返回。同样以上的例子,我们可以在不声明inline修饰符的情况下,这么做来实现相同的效果: + +```kotlin +fun main(args: Array) { + foo { return@foo } +} +fun foo(returning: () -> Unit) { + println("before local return") + returning() + println("after local return") + return +} +// 运行结果 +before local return +``` + +非局部返回尤其在循环控制中显得特别有用,比如Kotlin的forEach接口,它接收的就是一个Lambda参数,由于它也是一个内联函数,所以我们可以直接在它调用的Lambda中执行return退出上一层的程序。 + +```kotlin +fun hasZeros(list: List): Boolean { + list.forEach { + if (it == 0) return true // 直接返回foo函数结果 + } + return false +} +``` + +#### crossinline + +值得注意的是,非局部返回虽然在某些场合下非常有用,但可能也存在危险。因为有时候,我们内联的函数所接收的Lambda参数常常来自于上下文其他地方。为了避免带有return的Lambda参数产生破坏,我们还可以使用crossinline关键字来修饰该参数,从而杜绝此类问题的发生。就像这样子: + +```kotlin +fun main(args: Array) { + foo { return } +} +inline fun foo(crossinline returning: () -> Unit) { + println("before local return") + returning() + println("after local return") + return +} +// 运行结果 +Error: (2, 11) Kotlin: 'return' is not allowed here +``` + +#### 具体化参数类型 + +除了非局部返回之外,内联函数还可以帮助Kotlin实现具体化参数类型。Kotlin与Java一样,由于运行时的类型擦除,我们并不能直接获取一个参数的类型。然而,由于内联函数会直接在字节码中生成相应的函数体实现,这种情况下我们反而可以获得参数的具体类型。我们可以用reified修饰符来实现这一效果。 + +```kotlin +fun main(args: Array) { + getType() +} +inline fun getType() { + print(T::class) +} +// 运行结果 +class kotlin.Int +``` + +这个特性在Android开发中也格外有用。比如在Java中,当我们要调用startActivity时,通常需要把具体的目标视图类作为一个参数。然而,在Kotlin中,我们可以用reified来进行简化: + +```kotlin +inline fun Activity.startActivity() { + startActivity(Intent(this, T::class.java)) +} +``` + +这样,我们进行视图导航就非常容易了,如: + +```kotlin +startActivity() +``` + + + + + + + + + +[上一篇:Kotlin学习教程(六)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AD).md) +[下一篇:Kotlin学习教程(八)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AB).md) + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/KotlinCourse/3.Kotlin_\346\225\260\345\255\227&\345\255\227\347\254\246\344\270\262&\346\225\260\347\273\204&\351\233\206\345\220\210.md" "b/KotlinCourse/3.Kotlin_\346\225\260\345\255\227&\345\255\227\347\254\246\344\270\262&\346\225\260\347\273\204&\351\233\206\345\220\210.md" new file mode 100644 index 00000000..44f17930 --- /dev/null +++ "b/KotlinCourse/3.Kotlin_\346\225\260\345\255\227&\345\255\227\347\254\246\344\270\262&\346\225\260\347\273\204&\351\233\206\345\220\210.md" @@ -0,0 +1,554 @@ +Kotlin学习教程(三) +=== + +前面介绍了基本语法和编码规范后,接下来学习下基本类型。 + +在`Kotlin`中,所有东西都是对象,在这个意义上讲我们可以在任何变量上调用成员函数和属性。 一些类型可以有特殊的内部表示——例如, +数字、字符和布尔值可以在运行时表示为原生类型值,但是对于用户来说,它们看起来就像普通的类。 在本节中,我们会描述`Kotlin`中使用的基本类型: +数字、字符、布尔值、数组与字符串。 + +### 数字 + +`Kotlin`处理数字在某种程度上接近`Java`,但是并不完全相同。例如,对于数字没有隐式拓宽转换(如`Java`中`int`可以隐式转换为`long`), +另外有些情况的字面值略有不同。 + +`Kotlin`提供了如下的内置类型来表示数字: + +``` +Type Bit width +Double 64 +Float 32 +Long 64 +Int 32 +Short 16 +Byte 8 +```` + +注意在`Kotlin`中字符不是数字,字符用`Char`类型表示。它们不能直接当作数字 + + +### 字面常量 + +数值常量字面值有以下几种: +- 十进制:123 +- `Long`类型用大写`L`标记:`123L` +- 十六进制:`0x0F` +- 二进制:`0b00001011` + +注意: 不支持八进制 + +`Kotlin`同样支持浮点数的常规表示方法: + +默认`double`:123.5、123.5e10,`Float`用`f`或者`F`标记:`123.5f` + +你可以使用下划线使数字常量更易读 + +```kotlin +val oneMillion = 1_000_000 +val creditCardNumber = 1234_5678_9012_3456L +val socialSecurityNumber = 999_99_9999L +val hexBytes = 0xFF_EC_DE_5E +val bytes = 0b11010010_01101001_10010100_10010010 +``` + +### 显式转换 + +由于不同的表示方式,较小类型并不是较大类型的子类型。 如果它们是的话,就会出现下述问题: + +```kotlin +// 假想的代码,实际上并不能编译: +val a: Int? = 1 // 一个装箱的 Int (java.lang.Integer) +val b: Long? = a // 隐式转换产生一个装箱的 Long (java.lang.Long) +print(a == b) // 惊!这将输出“false”鉴于 Long 的 equals() 检测其他部分也是 Long +``` + +所以同一性还有相等性都会在所有地方悄无声息地失去。 +因此较小的类型不能隐式转换为较大的类型。 这意味着在不进行显式转换的情况下我们不能把`Byte`型值赋给一个`Int`变量。 + +```kotlin +val b: Byte = 1 // OK, 字面值是静态检测的 +val i: Int = b // 错误 +``` + +我们可以显式转换来拓宽数字 +```kotlin +val i: Int = b.toInt() // OK: 显式拓宽 +``` + +每个数字类型支持如下的转换: + +```kotlin +toByte(): Byte +toShort(): Short +toInt(): Int +toLong(): Long +toFloat(): Float +toDouble(): Double +toChar(): Char +``` + +### 运算 + +这是完整的位运算列表(只用于`Int`和`Long`): + +```kotlin +shl(bits) – 有符号左移 (Java 的 <<) +shr(bits) – 有符号右移 (Java 的 >>) +ushr(bits) – 无符号右移 (Java 的 >>>) +and(bits) – 位与 +or(bits) – 位或 +xor(bits) – 位异或 +inv() – 位非 +相等性检测:a == b 与 a != b +比较操作符:a < b、 a > b、 a <= b、 a >= b +区间实例以及区间检测:a..b、 x in a..b、 x !in a..b +|| – 短路逻辑或 +&& – 短路逻辑与 +! - 逻辑非 +``` + + +### 字符串 + +字符串用`String`类型表示。字符串是不可变的。字符串的元素——字符可以使用索引运算符访问:`s[i]`。可以用`for`循环迭代字符串: + +```kotlin +for (c in str) { + println(c) +} +``` +`Kotlin`有两种类型的字符串字面值: 转义字符串可以有转义字符,以及原生字符串可以包含换行和任意文本。转义字符串很像`Java`字符串: +```kotlin +val s = "Hello, world!\n" +``` +转义采用传统的反斜杠方式。 + +原生字符串 使用三个引号`"""`分界符括起来,内部没有转义并且可以包含换行和任何其他字符: + +```kotlin +val text = """ + for (c in "foo") + print(c) +""" +``` +你可以通过`trimMargin()`函数去除前导空格: + +```kotlin +val text = """ + |Tell me and I forget. + |Teach me and I remember. + |Involve me and I learn. + |(Benjamin Franklin) + """.trimMargin() +``` + +### 字符串模板 + +字符串可以包含模板表达式,即一些小段代码,会求值并把结果合并到字符串中。模板表达式以美元符`$`开头,由一个简单的名字构成: + +```kotlin +val i = 10 +val s = "i = $i" // 求值结果为 "i = 10" +``` + +或者用花括号括起来的任意表达式: +```kotlin +val s = "abc" +val str = "$s.length is ${s.length}" // 求值结果为 "abc.length is 3" +``` + +### 字符串判等 + +Kotlin中的判等性主要有两种类型: + +- 结构相等。通过操作符==来判断两个对象的内容是否相等。 +- 引用相等。通过操作符===来判断两个对象的引用是否一样,与之相反的判断操作符是!==。如果比较的是运行时的原始类型,比如Int,那么===判断的效果也等价于==。 + +```kotlin +var a = "Java" +var b = "Java" +var c = "Kotlin" +var d = "Kot" +var e = "lin" +var f = d + e + +a == b // true +a === b // true +c == f // true +c === f // false +``` + + + +### 引用相等 + +引用相等由`===`以及其否定形式`!===`操作判断。`a === b`当且仅当`a`和`b`指向同一个对象时求值为`true`。 + +### 结构相等 + +结构相等由`==`以及其否定形式`!==`操作判断。按照惯例,像`a == b`这样的表达式会翻译成 +`a?.equals(b) ?: (b === null)` +也就是说如果`a`不是`null`则调用`equals(Any?)`函数,否则即`a`是`null`检查`b`是否与`null`引用相等。 + +```kotlin +val a: Int = 10000 +print(a === a) // 输出“true” +val boxedA: Int? = agaomnh +val anotherBoxedA: Int? = a +print(boxedA === anotherBoxedA) // !!!输出“false”!!! +``` + +另一方面,它保留了相等性: + +```kotlin +val a: Int = 10000 +print(a == a) // 输出“true” +val boxedA: Int? = a +val anotherBoxedA: Int? = a +print(boxedA == anotherBoxedA) // 输出“true” +``` + + + +### 修饰符 + +`Kotlin`中修饰符是与`Java`中的有些不同。在`kotlin`中默认的修饰符是`public`,这节约了很多的时间和字符。 + +- `private` + `private`修饰符是最限制的修饰符,和`Java`中`private`一样。它表示它只能被自己所在的文件可见。所以如果我们给一个类声明为`private`, + 我们就不能在定义这个类之外的文件中使用它。 + 另一方面,如果我们在一个类里面使用了private修饰符,那访问权限就被限制在这个类里面了。甚至是继承这个类的子类也不能使用它。 + +- `protected`. + 在Java中是包、类及子类可访问,而在Kotlin中只允许类及子类。 + +- `internal` + 它与Java的default有点像但也有所区别。如果是一个定义为`internal`的包成员的话,对所在的整个`module`可见。如果它是一个其它领域的成员,它就需要依赖那个领域的可见性了。 + 比如如果写了一个`private`类,那么它的`internal`修饰的函数的可见性就会限制与它所在的这个类的可见性。 + +- `public`. + 你应该可以才想到,这是最没有限制的修饰符。这是默认的修饰符,成员在任何地方被修饰为public,很明显它只限制于它的领域。 + +### 数组 + +数组用类`Array`实现,并且还有一个`size`属性及`get`和`set`方法,由于使用`[]`重载了`get`和`set`方法,所以我们可以通过下标很方便的获取或者 +设置数组对应位置的值。 +`Kotlin`标准库提供了`arrayOf()`创建数组和`xxArrayOf`创建特定类型数组 + +```kotlin +val array = arrayOf(1, 2, 3) +val countries = arrayOf("UK", "Germany", "Italy") +val numbers = intArrayOf(10, 20, 30) +val array1 = Array(10, { k -> k * k }) +val longArray = emptyArray() +val studentArray = Array(2) +studentArray[0] = Student("james") +``` + +和`Java`不一样的是`Kotlin`的数组是容器类,提供了`ByteArray`,`CharArray`,`ShortArray`,`IntArray`,`LongArray`,`BooleanArray`, +`FloatArray`和`DoubleArray`。 + +### 集合 + +`Kotlin`的`List`类型是一个提供只读操作如`size`、`get`等的接口。和`Java`类似,它继承自`Collection`进而继承自`Iterable`。 +改变`list`的方法是由`MutableList`加入的。这一模式同样适用于`Set/MutableSet`及`Map/MutableMap`。 + +可变集合,顾名思义,就是可以改变的集合。可变集合都会有一个修饰前缀“Mutable”,比如MutableList。这里的改变是指改变集合中的元素,比如以下可变集合: + +```kotlin +val list = mutableListOf(1, 2, 3, 4, 5) +list[0] = 0 // 变成[0, 2, 3, 4, 5] +``` + + + +`Kotlin`没有专门的语法结构创建`list`或`set`。要用标准库的方法如`listOf()`、`mutableListOf()`、`setOf()`、`mutableSetOf()`。 +创建`map`可以用`mapOf(a to b, c to d)`。 + +```kotlin +fun main(args : Array) { + var lists = listOf("a", "b", "c") + for(list in lists) { + println(list) + } +} +``` + +```kotlin +fun main(args : Array) { + var map = TreeMap() + map["0"] = "0 haha" + map["1"] = "1 haha" + map["2"] = "2 haha" + + println(map["1"]) +} +``` + +```kotlin +val numbers: MutableList = mutableListOf(1, 2, 3) +val readOnlyView: List = numbers +println(numbers) // 输出 "[1, 2, 3]" +numbers.add(4) +println(readOnlyView) // 输出 "[1, 2, 3, 4]" +readOnlyView.clear() // -> 不能编译 + +val strings = hashSetOf("a", "b", "c", "c") +assert(strings.size == 3) +``` + +Kotlin中提供了很多操作结合的函数,例如: + +```kotlin +val newList = list.map{it * 2} // 对集合遍历,在遍历过程中,给每个元素都乘以2,得到一个新的集合 + +val mStudents = students.filter{it.sex == "m"} // 筛选出性别为男的学生 + +val scoreTotal = students.sumBy{it.score} // 拥挤和中的sumby实现求和 +``` + +#### 通过序列提高效率 + +```kotlin +val list = listOf(1, 2, 3, 4, 5) +list.filter {it > 2}.map {it * 2} +``` + +上面的写法很简洁,在处理集合时,类似于上面的操作能够帮助我们解决大部分的问题。但是list中的元素非常多的时候(比如超过10万),上面的操作在处理集合的时候就会显得比较低效。因为filter方法和map方法都会返回一个新的集合,也就是说上面的操作会产生两个临时集合,因为list会先调用filter方法,然后产生的集合会再次调用map方法。如果list中的元素非常多,这将会是一笔不小的开销。为了解决这一问题,序列(Sequence)就出现了。 + +```kotlin +list.asSequence().filter {it > 2}.map {it * 2}.toList() +``` + +首先通过asSequence方法将一个列表转换为序列,然后在这个序列上进行相应的操作,最后通过toList方法将序列转为列表。将list转换为序列,在很大程度上就提高了上面操作集合的效率。因为在使用序列的时候filter方法和map方法的操作都没有创建额外的集合,这样当集合中的元素数量巨大的时候,就减少了大部分开销。在Kotlin中,序列中元素的求值是惰性的,这就意味着在利用序列进行链式求值的时候,不需要像操作普通集合那样,每进行一次求值操作,就产生一个新的集合保存中间数据。那么什么惰性又是什么意思呢? + +在编程语言理论中,惰性求值(Lazy Evaluation)表示一种在需要时才进行求值的计算方式。在使用惰性求值的时候,表达式不在它被绑定到变量之后就立即求值,而是在该值被取用时才去求值。通过这种方式,不仅能得到性能上的提升,还有一个重要的好处就是它可以构造出一个无限的数据类型。 + + + +#### 序列的操作方式 + +```kotlin +list.asSequence().filter {it > 2}.map {it * 2}.toList() +``` + +在这个例子中,我们序列总共执行了两类操作分别是: + +- `filter{it > 2}.map{it * 2}`:filter和map的操作返回的都是序列,我们将这类操作称为中间操作。 +- `toList()`:这一类操作将序列转换为List,我们将这类操作称为末端操作。 + +其实,Kotlin中序列的操作就分为两类: + +- 中间操作 + + 中间操作都是采用惰性求值的,例如: + + ```kotlin + list.asSequence().filter { + println("filter($it)") + }.map { + println("map($it)") + } + ``` + + 上面操作中的println方法根本没有被执行,这说明filter和map方法的执行被延迟了,这就是惰性求值的体现。惰性求值也被称为延迟求值,通过前面的定义我们知道,惰性求值仅仅在该值被需要的时候才会真正去求值。那么这个”被需要“的状态怎么去触发呢?这就需要另外一个操作了-末端操作。 + +- 末端操作 + + 在对集合进行操作的时候,大部分情况下,我们在意的只是结果,而不是中间过程。末端操作就是一个返回结果的操作,它的返回值不能是序列,必须是一个明确的结果,比如列表、数字、对象等表意明确的结果。末端操作一般都放在链式操作的末尾,在执行末端操作的时候,会去触发中间操作的延迟计算,也就是将”被需要“这个状态打开了,我们给上面的例子加上末端操作: + + ```kotlin + list.asSequence().filter { + println("filter($it)") + it > 2 + }.map { + println("map($it)") + it * 2 + }.toList() + // 结果 + filter(1) + filter(2) + filter(3) + map(3) + filter(4) + map(4) + filter(5) + map(5) + [6, 8, 10] + ``` + + 可以看到,所有的中间操作都被执行了。从上面执行打印的结果我们发现,它的执行顺序与我们预想的不一样。普通集合在进行链式操作的时候会先在list上调用filter,然后产生一个结果列表,接下来map就在这个结果列表上进行操作。而序列则不一样,序列在执行链式操作的时候,会将所有的操作都应用在一个元素上,也就是说,第一个元素执行完所有的操作之后,第二个元素再去执行所有的操作,以此类推。放映到我们这个例子上面,就是第一个元素执行了filter之后再去执行map,然后第二个元素也是这样。 + +#### 序列可以是无限的 + +在介绍惰性求值的时候,我们提过一点,就是惰性求值最大的好处是可以构造出一个无限的数据类型。那么我们能否使用序列来构造出一个无限的数据类型呢?答案是肯定的。 + +那接下来,该怎么去实现一个自然数数列呢?采用一般的列表肯定是不行的,因为构造一个列表必须列举出列表中的元素,而我们是没有办法将自然数全部列举出来的。 + +我们知道,自然数是有一定规律的,就是最后一个数永远是前一个数加1的结果,我们只需要实现一个列表,让这个列表描述这种规律,那么也就相当于实现了一个无限的自然数数列。好看Kotlin也给我们提供了这样一个方法,去创建无限的数列: + +```kotlin +val naturalNumList = generateSequence(0) { it + 1} +``` + +通过上面这一行代码,我们就非常简单的实现了自然数数列,上面我们调用了一个方法generateSequence来创建序列。我们知道序列是惰性求值的,所以上面创建的序列是不会把所有的自然数都列举出来的,只有在我们调用一个末端操作的时候,才去列举我们所需要的列表。比如我们要从这个自然数列表中取出前10个自然数: + +```kotlin +naturalNumList.takeWhile{it <= 9}.toList() +[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] +``` + + + + + +### 可`null`类型 + + +因为在`Kotlin`中一切都是对象,一切都是可`null`的。当某个变量的值可以为`null`的时候,必须在声明处的类型后添加`?`来标识该引用可为空。 +`Kotlin`通过`?`将是否允许为空分割开来,比如`str:String`为不能空,加上`?`后的`str:String?`为允许空,通过这种方式,将本是不能确定的变 +量人为的加入了限制条件。而不符合条件的输入,则会在`IDE`上显示编译错误而无法执行。 + +```kotlin +var value1: String +value1 = null // 编译错误 Null can not be a value of a non-null type String + +var value2 : String? +value2 = null // 编译通过 +``` + +在对变量进行操作时,如果变量是可能为空的,那么将不能直接调用,因为编译器不知道你的变量是否为空,所以编译器就要求你一定要对变量进行判断 + +```kotlin +var str : String? = null +// 编译错误 Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String? +str.length +// 编译能通过,这表示如果str不为空的时候执行length方法 +str?.length +``` + +那么问题来了,我们知道在`java`中`String.length`返回的是`int`,上面的`str?.length`既然编译通过了,那么它返回了什么?我们可以这么写: + +`var result = str?.length` + +这么写编译器是能通过的,那么`result`的类型是什么呢?在`Kotlin`中,编译器会自动根据结果判断变量的类型,翻译成普通代码如下: + +```kotlin +if(str == null) + result = null; // 这里result为一个引用类型 +else + result = str.length; // 这里result为Int +``` + +那么如果我们需要的就是一个`Int`的结果(事实上大部分情况都是如此),那又该怎么办呢?在`kotlin`中除了`?`表示可为空以外,还有一个新的符号`:`双 +感叹号`!!`,表示一定不能为空。所以上面的例子,如果要对`result`进行操作,可以这么写: + +```kotlin +var str : String? = null +var result : Int = str!!.length +``` + +这样的话,就能保证`result`的数据类型,但是这样还有一个问题,那就是`str`的定义是可为空的,上面的代码中,`str`就是空,这时候下面的操作虽然 +不会报编译异常,但是运行时就会见到我们熟悉的空指针异常`NullPointerExectpion`,这显然不是我们希望见到的,也不是`kotlin`愿意见到的。 +`java`中的三元操作符大家应该都很熟悉了,`kotlin`中也有类似的,它很好的解决了刚刚说到的问题。在`kotlin`中,三元操作符是`?:`,写起来也 +比`java`要方便一些。 + +```kotlin +var str : String? = null +var result = str?.length ?: -1 +//等价于 +var result : Int = if(str != null) str.length else -1 +``` + +`if null`缩写 + +```kotlin +val data = …… +val email = data["email"] ?: throw IllegalStateException("Email is missing!") +``` + +如果`?:`左侧表达式非空,`elvis`操作符就返回其左侧表达式,否则返回右侧表达式。 +请注意,当且仅当左侧为空时,才会对右侧表达式求值。 + + +##### `!!`操作符 + +我们可以写`b!!`,这会返回一个非空的`b`值 +(例如:在我们例子中的`String`)或者如果`b`为空,就会抛出一个空指针异常: + +```kotlin +val l = b!!.length +``` + +因此,如果你想要一个 NPE,你可以得到它,但是你必须显式要求它,否则它不会不期而至。 + + +#### 安全的类型转换 + +如果对象不是目标类型,那么常规类型转换可能会导致`ClassCastException`。 +另一个选择是使用安全的类型转换,如果尝试转换不成功则返回`null{: .keyword }`: + +```kotlin +val aInt: Int? = a as? Int +``` + +#### 可空类型的集合 + +如果你有一个可空类型元素的集合,并且想要过滤非空元素,你可以使用`filterNotNull`来实现。 + +```kotlin +val nullableList: List = listOf(1, 2, null, 4) +val intList: List = nullableList.filterNotNull() +``` + + + +### 使用类型检测及自动类型转换 + +`is`运算符检测一个表达式是否某类型的一个实例。 如果一个不可变的局部变量或属性已经判断出为某类型,那么检测后的分支中可以直接当作该类型使用, +无需显式转换: + +```kotlin +fun getStringLength(obj: Any): Int? { + if (obj !is String) return null + + // `obj` 在这一分支自动转换为 `String`,这是因为Kotlin的编译器帮我们做了转换 + // 这称为Kotlin中的智能转换(Smart Casts)。官方文档中这样介绍: 当且仅当Kotlin的编译器 + // 确定在类型检查后该变量不会再改变,才会产生Smart Casts。 + return obj.length +} +``` + +### 返回和跳转 + +`Kotlin`有三种结构化跳转表达式: + +- `return`:默认从最直接包围它的函数或者匿名函数返回。 +- `break`:终止最直接包围它的循环。 +- `continue`:继续下一次最直接包围它的循环。 + +在`Kotlin`中任何表达式都可以用标签`label`来标记。标签的格式为标识符后跟`@`符号,例如:`abc@`、`fooBar@`都是有效的标签。 + +要为一个表达式加标签,我们只要在其前加标签即可。 + +```kotlin +loop@ for (i in 1..100) { + for (j in 1..100) { + if (……) break@loop + } +} +``` + + + + + +[上一篇:Kotlin学习教程(二)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%8C).md) +[下一篇:Kotlin学习教程(四)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%9B%9B).md) + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! + diff --git "a/KotlinCourse/4.Kotlin_\350\241\250\350\276\276\345\274\217&\345\205\263\351\224\256\345\255\227.md" "b/KotlinCourse/4.Kotlin_\350\241\250\350\276\276\345\274\217&\345\205\263\351\224\256\345\255\227.md" new file mode 100644 index 00000000..43178b4e --- /dev/null +++ "b/KotlinCourse/4.Kotlin_\350\241\250\350\276\276\345\274\217&\345\205\263\351\224\256\345\255\227.md" @@ -0,0 +1,224 @@ +Kotlin学习教程(四) +=== + +## `if`表达式 + +在`Kotlin`中,`if`是一个表达式,即它会返回一个值。因此就不需要三元运算符`条件 ? 然后 : 否则`,因为普通的`if`就能胜任这个角色。 +`if`的分支可以是代码块,最后的表达式作为该块的值: + +```kotlin +val max = if (a > b) { + print("Choose a") + a +} else { + print("Choose b") + b +} +``` + +## `when`表达式 + +`when`表达式与`Java`中的`switch/case`类似,但是要强大得多。这个表达式会去试图匹配所有可能的分支直到找到满意的一项。然后它会运行右边的表达 +式。 +与`Java`的`switch/case`不同之处是参数可以是任何类型,并且分支也可以是一个条件。 + +对于默认的选项,我们可以增加一个`else`分支,它会在前面没有任何条件匹配时再执行。条件匹配成功后执行的代码也可以是代码块: +```kotlin +when (x){ + 1 -> print("x == 1") + 2 -> print("x == 2") + else -> { + print("I'm a block") + print("x is neither 1 nor 2") + } +} +``` + +因为它是一个表达式,它也可以返回一个值。我们需要考虑什么时候作为一个表达式使用,它必须要覆盖所有分支的可能性或者实现`else`分支。否则它不会被 +编译成功: + +```kotlin +val result = when (x) { + 0, 1 -> "binary" + else -> "error" +} +``` + +如你所见,条件可以是一系列被逗号分割的值。但是它可以更多的匹配方式。比如,我们可以检测参数类型并进行判断: + +```kotlin +when(view) { + is TextView -> view.setText("I'm a TextView") + is EditText -> toast("EditText value: ${view.getText()}") + is ViewGroup -> toast("Number of children: ${view.getChildCount()} ") + else -> view.visibility = View.GONE +} +``` + +### 示例:when对if else的改造 + +```kotlin +fun schedule(day: Day, sunny: Boolean) = { + if (day == Day.SAT) { + basketball() + } else if (day == Day.SUN) { + fishing() + } else if (day == Day.FRI) { + appointment() + } else { + if (sunny) { + library() + } else { + study() + } + } +} +``` + +上面的例子中因为存在不少if else分支,代码显得不够优雅,更好的改进方法是用when表达式来优化: + +```kotlin +fun schedule(sunny: Boolean, day: Day) = when (day) { + Day.SAT -> basketball() + Day.SUN -> fishing() + Day.FRI -> appointment() + else -> when { + // when关键字的参数可以省略 + sunny -> library() + else -> study() + } +} +``` + +一个完整的when表达式类似switch语句,由when关键字开始,用花括号包含多个逻辑分支,每个分支由-> 连接,不再需要switch的break(这真是一个恼人的关键字),由上往下匹配,一直匹配完为止,否则执行else分支的逻辑,类似switch的default。 + +到这里你可能会说上面的例子中,这样嵌套子when表达式,层次依旧比较深。要知道when表达式是很灵活的,我们很容易通过如下修改来解决这个问题: + +```kotlin +fun schedule(sunny: Boolean, day: Day) = when { + day == Day.SAT -> basketball() + day == Day.SUN -> fishing() + day == Day.FRI -> appointment() + sunny -> library() + else -> study() +} +``` + +这样就会更优雅了。 + +## for循环 + +```kotlin +val items = listOf("apple", "banana", "kiwi") +for (item in items) { + println(item) +} + +for (i in array.indices) + print(array[i]) +``` + +在Kotlin中用in关键字来检查一个元素是否是一个区间或集合中的成员。如果我们在in前面加上感叹号,那么就是相反的判断结果。 + +### Ranges + +`Range`表达式使用一个`..`操作符。表示就是一个该范围内的数据的数组,包含头和尾 + +```kotlin +var nums = 1..100 +for(num in nums) { + println(num) + // 打印出1 2 3 ....100 +} +``` + +```kotlin +if(i >= 0 && i <= 10) + println(i) +``` + +转换成 + +```kotlin +if (i in 0..10) + println(i) +``` + +Ranges默认会自增长,所以如果像以下的代码: + +```kotlin +for (i in 10..0) + println(i) +``` + +它就不会做任何事情。但是你可以使用`downTo`函数: + +```kotlin +for(i in 10 downTo 0) + println(i) +``` + +我们可以在`Ranges`中使用`step`来定义一个从`1`到一个值的不同的空隙: + +```kotlin +for (i in 1..4 step 2) println(i) +for (i in 4 downTo 1 step 2) println(i) +``` + +### Until + +上面的`Range`是包含了头和尾,那如果只想包含头不包含尾呢? 就要用`until` + +```kotlin +var nums = 1 until 100 +for(num in nums) { + println(num) + // 这样打印出来是1 2 3 .....99 +} +``` + + + +上面in、step、downTo、until这几个,他们可以不通过点号,而是通过中缀表达式来被调用,从而让语法变得更加简洁直观。 + + + +### `Kotlin`用到的关键字 + +- `var`:定义变量 +- `val`:定义常量 +- `fun`:定义方法 +- `Unit`:默认方法返回值,类似于`Java`中的`void`,可以理解成返回没什么用的值 +- `vararg`:可变参数 +- `$`:字符串模板(取值) +- 位运算符:`or`(按位或),`and`(按位与),`shl`(有符号左移),`shr`(有符号右移), +- `ushr`(无符号右移),`xor`(按位异或),`inv`(按位取反) +- `in`:在某个范围中 检查值是否在或不在(`in/!in`)范围内或集合中 +- `downTo`:递减,循环时可用,每次减1 +- `step`:步长,循环时可用,设置每次循环的增加或减少的量 +- `when`:`Kotlin`中增强版的`switch`,可以匹配值,范围,类型与参数 +- `is`:判断类型用,类似于`Java`中的`instanceof()`,`is`运算符检查表达式是否是类型的实例。 如果一个不可变的局部变量或属性是指定类型, + 则不需要显式转换 +- `private`仅在同一个文件中可见 +- `protected`同一个文件中或子类可见 +- `public`所有调用的地方都可见 +- `internal`同一个模块中可见 +- `abstract`抽象类标示 +- `final`标示类不可继承,默认属性 +- `enum`标示类为枚举 +- `open`类可继承,类默认是`final`的 +- `annotation`注解类 +- `init`主构造函数不能包含任何的代码。初始化的代码可以放到以`init`关键字作为前缀的初始化块(`initializer blocks`)中 +- `field`只能用在属性的访问器内。特别注意的是,`get set`方法中只能能使用`filed`。属性访问器就是`get set`方法。 +- `:`用于类的继承,变量的定义 +- `..`围操作符(递增的) `1..5`,`2..6`千万不要`6..2` +- `::`作用域限定符 +- `inner`类可以标记为`inner {: .keyword }`以便能够访问外部类的成员。内部类会带有一个对外部类的对象的引用 +- `object`对象声明并且它总是在`object{: .keyword }`关键字后跟一个名称。对象表达式:在要创建一个继承自某个(或某些)类型的匿名类的对象会 + 用到 + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! + diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\272\224).md" "b/KotlinCourse/5.Kotlin_\345\206\205\351\203\250\347\261\273&\345\257\206\345\260\201\347\261\273&\346\236\232\344\270\276&\345\247\224\346\211\230.md" similarity index 62% rename from "KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\272\224).md" rename to "KotlinCourse/5.Kotlin_\345\206\205\351\203\250\347\261\273&\345\257\206\345\260\201\347\261\273&\346\236\232\344\270\276&\345\247\224\346\211\230.md" index 8f1e87bb..4bcc58cc 100644 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\272\224).md" +++ "b/KotlinCourse/5.Kotlin_\345\206\205\351\203\250\347\261\273&\345\257\206\345\260\201\347\261\273&\346\236\232\344\270\276&\345\247\224\346\211\230.md" @@ -95,9 +95,15 @@ val outter = Outter() outter.Inner().execute() ``` +#### 内部类vs嵌套类 +在Java中,我们通过在内部类的语法上增加一个static关键词,把它变成一个嵌套类。然而,Kotlin则是相反的思路,默认是一个嵌套类,必须加上inner关键字才是一个内部类,也就是说可以把静态的内部类看成嵌套类。 -匿名内部类 +内部类和嵌套类有明显的差别,具体体现在:内部类包含着对其外部类实例的引用,在内部类中我们可以使用外部类中的属性。而在嵌套类中不包含对其外部类实例的引用,所以它无法调用其外部类的属性。 + + + +#### 匿名内部类 ```kotlin // 通过对象表达式来 创建匿名内部类的对象,可以避免重写抽象类的子类和接口的实现类,这和Java中匿名内部类的是接口和抽象类的延伸一致。 @@ -124,8 +130,9 @@ mViewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { }) ``` +### 枚举 -### 枚举 +与Java中的enum语法大体类似,无非多了一个class关键字,表示它是一个枚举类。 ```kotlin enum class Day { @@ -133,16 +140,23 @@ enum class Day { THURSDAY, FRIDAY, SATURDAY } ``` -枚举可以带参数: +不过Kotlin中的枚举类当然没有那么简单,由于它是一种类,我们可以猜测它自然应该可以拥有构造函数,以及定义额外的属性和方法。 ```kotlin -enum class Icon(val res: Int) { - UP(R.drawable.ic_up), - SEARCH(R.drawable.ic_search), - CAST(R.drawable.ic_cast) +enum class DayOfWeek(val day: Int) { + MON(1), + TUE(2), + WEN(3), + THU(4), + FRI(5), + SAT(6), + SUN(7) + ; // 需要注意的是,当在枚举类中存在额外的方法或或属性定义,则必须强制加上分号,虽然你可能不会喜欢这个语法 + fun getDayNumber(): Int { + return day + } } -val searchIconRes = Icon.SEARCH.res ``` 枚举可以通过`String`匹配名字来获取,我们也可以获取包含所有枚举的`Array`,所以我们可以遍历它。 ```kotlin @@ -157,6 +171,42 @@ val searchPosition: Int = Icon.SEARCH.ordinal() ### 密封类 +Kotlin除了可以利用final来限制类的继承之外,还可以通过密封类的语法来限制一个类的继承,比如: + +```kotlin +sealed class Bird { + open fun fly() = "I can fly" + class Eagle : Bird() +} +``` + +Kotlin通过sealed关键字来修饰一个类为密封类,若要继承则需要将子类定义在同一个文件中,其他文件中的类将无法继承他。但这种方式也有它的局限性。即它不能被初始化,因为它背后是基于一个抽象类实现的,这一点可以从它转换后的Java代码看出: + +```java +public abstract class Bird { + @NotNull + public String fly() { + return "I can fly" + } + private Bird() { + + } + // $FF: synthetic method + public Bird(DefaultConstructorMarker $constructor_maker) { + this(); + } + + public static final class Eagle extends Bird { + public Eagle() { + super((DefaultConstructorMarker) null); + } + } +} + +``` + + + 密封类用来表示受限的类继承结构:当一个值为有限集中的 类型、而不能有任何其他类型时。在某种意义上,他们是枚举类的扩展:枚举类型的值集合 也是受限的,但每个枚举常量只存在一个实例,而密封类 @@ -229,8 +279,103 @@ val s = try { x as String } catch(e: ClassCastException) { null } ### 对象`(Object)` +在Java中,static是非常重要的特性,它可以用来修饰类、方法或属性。然而static修饰的内容都属于类的,而不是某个具体对象的,但在定义时却与普通的变量和方法混杂在一起,显得格格不入。 + +在Kotlin中,你将告别static这种语法,因为它引入了全新的关键字object,可以完美的代替使用static的所有场景。当然除了代替static的场景之外,它还能实现更多的功能,比如单例对象及简化匿名表达式等。 + +#### 伴生对象 + +先看一个Java例子: + +```java +public class Prize { + private String name; + private int count; + private int type; + + public Prize(String name, int count, int type) { + this.name = name; + this.count = count; + this.type = type; + } + + static int TYPE_REDPACK = 0; + static int TYPE_COUPON = 1; + + static boolean isRedpack(Prize prize) { + return prize.type == TYPE_REDPACK; + } + + public static void main(String[] args) { + Prize prize = new Prize("hongbao", 10, Prize.TYPE_REDPACK); + System.out.println(Prize.isRedpack(prize)); + } +} +``` + +上面是很常见的Java代码,也许你已经习惯了,但是如果仔细思考,会发现这种语法其实并不是非常好。因为在一个类中既有静态变量、静态方法,也有普通变量、普通方法的声明。然而,静态变量和静态方法是属于一个类的,普通变量、普通方法是属于一个具体对象的。虽然有static作为区分,然而在代码结构上职能并不是区分得很清晰。 + +那么,有没有一种方式能将这两部分代码清晰的分开,但又不失语义化呢?Kotlin中引入了伴生对象的概念,简单来说,这是一种利用companion object两个关键字创造的语法。 + +伴生对象:“伴生”是相较于一个类而言的,意为伴随某个类的对象,它属于这个类所有,因此伴生对象跟Java中static修饰效果性质一样,全局只有一个单例。它需要声明在类的内部,在类被装载时会被初始化。 + +现在将上面的例子改写成伴生对象的版本: + +```kotlin +class Prize(val name: String, val count: Int, val type: Int) { + companion object { + val TYPE_REDPACK = 0 + val TYPE_COUPON = 1 + + fun isRedpack(prize: Prize): Boolean { + return prize.type == TYPE_REDPACK + } + } + + fun main(args: Array) { + val prize = Prize("hongbao", 10, Prize.TYPE_REDPACK) + print(Prize.isRedpack(prize)) + } +} +``` + +可以发现,该版本在语义上更清晰了。而且,companion object用花括号包裹了所有静态属性和方法,使得它可以与Prize类的普通方法和属性清晰的区分开来。最后,我们可以使用点号来对一个类的静态的成员进行调用。 + +伴生对象的另一个作用是可以实现工厂方法模式。前面也说过如何从构造方法实现工厂方法模式,然而这种方法存在以下缺点: + +- 利用多个构造方法语义不够明确,只能靠参数区分 +- 每次获取对象时都需要重新创建对象 + +你会发现,伴生对象也是实现工厂方法模式的另一种思路,可以改进以上的两个问题。 + +```kotlin +class Prize private constructor(val name: String, val count: Int, val type: Int) { + companion object { + val TYPE_COMMON = 1 + val TYPE_REDPACK = 2 + val TYPE_COUPON = 3 + + val defaultCommonPrize = Prize("common", 10, Prize.TYPE_COMMON) + + fun newRedpackPrize(name: String, count: int) = Prize(name, count, Prize.TYPE_REDPACK) + fun newCouponPrize(name: String, count: Int) = Prize(name, count, Prize.TYPE_COUPON) + fun defaultCommonPrize() = defaultCommonPrize // 无须构造新对象 + } + + fun main(args: Array) { + val redpackPrize = Prize.newRedpackPrize("hongbao", 10) + val couponPrize = Prize.newCouponPrize("shiyuan", 10) + vval commonPrize = Prize.defaultCommonPrize() + } +} +``` + +总的来说,伴生对象是Kotlin中用来代替static关键字的一种方式,任何在Java类内部用static定义的内容都可以用Kotlin中的伴生对象来实现。然而,他们是类似的,一个类的伴生对象跟一个静态类一样,全局只能有一个。 + + + 声明对象就如同声明一个类,你只需要用保留字`object`替代`class`,其他都相同。只需要考虑到对象不能有构造函数,因为我们不调用任何构造函数来访问 -它们。事实上,对象就是具有单一实现的数据类型。 +它们。事实上,对象就是具有单一实现的数据类型。object声明的内容可以看成没有构造方法的类,它会在系统或者类加载时进行初始化。 ```kotlin object Resource { @@ -240,14 +385,59 @@ object Resource { ### 单例 +因为一个类的伴生对象跟一个静态类一样,全局只能有一个。这让我们联想到了什么? 没错,就是单例对象。 + ```kotlin object Resource { val name = "Name" } ``` -因为对象就是具有单一实现的数据类型,所以在`kotlin`中对象就是单例。 -对象的实例在我们第一次使用时,被创建。所以这里有一个懒惰实例化:如果一个对象永远不会被使用,这个实例永远不会被创建。 +因为对象就是具有单一实现的数据类型,所以在`kotlin`中对象就是单例。 单例对象会在系统加载的时候初始化,当然全局只有一个,所以它是饿汉式的单例。 + +看一下自动生成的java代码: + +```java +public final class SingleTon { + @NotNull + private static String name; + @NotNull + public static final SingleTon INSTANCE; + + @NotNull + public final String getName() { + return name; + } + + public final void setName(@NotNull String var1) { + Intrinsics.checkNotNullParameter(var1, ""); + name = var1; + } + + private SingleTon() { + } + + static { + SingleTon var0 = new SingleTon(); + INSTANCE = var0; + name = ""; + } +} +``` + +这里官网中对object的介绍是: object declarations are initialized lazily, when accessed for the first time + +为什么上面说它是饿汉式? 这个从上面反编译的代码可以看到确实有一个static的静态代码块,静态代码块具有懒惰加载特性,但它这个是加载特性,并不是我们说的: + +```java +public static Instance getInstance() { + if (instance == null) { + instance = new Instance(); + } +} +``` + +具体可以看: [Kotlin Object Declarations Are Initialized in Static Block](https://hanru-yeh.medium.com/kotlin-object-declarations-are-initialized-in-static-block-5e1c2e1c3401) ### 对象表达式 @@ -267,6 +457,10 @@ recycler.adapter = object : RecyclerView.Adapter() { ``` 例如,每次想要创建一个接口的内联实现,或者扩展另一个类时,你将使用上面的符号。 +object表达式和匿名内部类很像,那对象表达式与Lambda表达式哪个更适合代替匿名内部类呢? 当你的匿名内部类使用的类接口只需要实现一个方法时,使用Lambda表达式更适合。当匿名内部类内有多个方法实现的时候,使用object表达式更适合。 + + + ### 伴生对象`(Companion Object)` diff --git "a/KotlinCourse/6.Kotlin_\345\244\232\347\273\247\346\211\277\351\227\256\351\242\230.md" "b/KotlinCourse/6.Kotlin_\345\244\232\347\273\247\346\211\277\351\227\256\351\242\230.md" new file mode 100644 index 00000000..7017a62f --- /dev/null +++ "b/KotlinCourse/6.Kotlin_\345\244\232\347\273\247\346\211\277\351\227\256\351\242\230.md" @@ -0,0 +1,211 @@ +Kotlin学习教程之多继承问题 +=== + + + +继承和实现是面向对象程序设计中不变的主题。Java是不支持类的多继承的,Kotlin亦是如此。我们他们要这样设计呢? 现实中,其实多继承的需求经常会出现,然而类的多继承方式会导致进程关系上语义的混淆。 + + + +### 骡子的多继承困惑 + +如果你了解C++,应该知道C++中的类是支持多重继承机制的。然而,C++中存在一个经典的钻石问题--骡子的多继承困惑。我们假设Java的类也支持多继承,然后模仿C++中类似的语法,来看看它到底会导致什么问题: + +```java +abstract class Animal { + abstract public void run(); +} +class Horse extends Animal { + @Override + public void run() { + System.out.println("I am run very fast"); + } +} +class Donkey extends Animal { + @Override + public void run() { + System.out.println("I am run very slow"); + } +} +class Mule extends Horse, Donkey { + // 骡子 + ... +} +``` + +这是一段伪代码,这段代码的含义是: + +- 马和驴都继承了Animal类,并实现了Animal中的run抽象方法。 +- 骡子是马和驴的杂交产物,它拥有两者的特性,于是Mule利用多继承同时继承了Horse和Donkey + +目前看起来没有问题,然而当我们打算在Mule中实现run方法的时候,问题就产生了:Mule到底是继承Horse的run方法还是Donkey的run方法?这个就是经典的钻石问题。 + +![image-20210325104002353](https://raw.githubusercontent.com/CharonChui/Pictures/master/mule_problem.png?raw=true) + +所以钻石问题也被称为棱形继承问题。可以发现,类的多继承如果使用不当,就会在继承关系上产生歧义。而且,多继承还会给代码维护带来很多困扰:一来代码的耦合度会提高,二来各种类之间的关系令人眼花缭乱。 + +于是Kotlin和Java一样只支持类的单继承。那么面对多继承的需求,在Kotlin中该如何解决呢? + + + +#### 接口实现多继承 + +接口支持多实现,所以一个类可以实现多个接口,这是Java经常干的事。Kotlin的接口与Java和类似,但它除了可以定义带默认实现的方法之外,还可以声明抽象的属性。下面就是用Kotlin中的接口来实现多继承: + +```kotlin +interface Flyer { + fun fly() + fun kind() = "flying animals" +} +interface Animal { + val name: String + fun eat() + fun kind() = "flying animals" +} + +class Bird(override val name: String) : Flyer, Animal { + override fun eat() { + println("I can eat") + } + override fun fly() { + println("I can fly") + } + override fun kind() = super.kind() +} + +fun main(args: Array.kind()。当然我们也可以主动实现方法,覆盖父接口的方法。如: + +```kotlin +override fun kind() = "a flying ${this.name}" +// 最终的执行结果就是 +a flying sparrow +``` + +通过这个例子,可以看出实现接口的语法: + +- 在Kotlin中实现一个接口时,需要实现接口中没有默认实现的方法及未初始化的属性,若同时实现多个接口,而接口间又有相同方法名的默认实现时,则需要主动指定使用哪个接口的方法或者重写方法。 +- 如果是默认的接口方法,你可以在实现类中通过super这种方式调用它,其中T为拥有该方法的接口名。 +- 在实现接口的属性和方法时,都必须带上override关键字,不能省略。 + +#### 内部类解决多继承问题 + +在Java中可以将一个类的定义放在另一个类的定义内部,这就是内部类。由于内部类可以继承一个与外部无关的类,所以这保证了内部类的独立性,可以用它这个特性来尝试解决多继承的问题。 + +```kotlin +open class Horse { + fun runFast() { + println("I can run fast") + } +} +open class Donkey { + fun doLongTimeThing() { + println("I can do some thing long time") + } +} +class Mule { + fun runFast() { + HorseC().runFast() + } + fun doLongTimeThing() { + DonkeyC().doLongTimeThing() + } + + private inner class HorseC : Horse() + private inner class DonkeyC : Donkey() +} +``` + +上面的例子可以看到: + +- 可以在一个类的内部定义多个内部类,每个内部类的实例都有自己的独立状态,它们与外部对象的信息相互独立。 +- 通过让内部类HorseC、DonkeyC分别继承Horse和Donkey这两个外部类,我们可以在Mule类中定义它们的实例对象,从而获得了Horse和Donkey两者不同的状态和行为。 +- 可以利用private修饰内部类,使得其他类都不能访问内部类,这样可以具有非常良好的封闭性。 + +所以在某些场合下,内部类确实是一种解决多继承非常好的思路。 + + + +#### 使用委托代替多继承 + +委托是一种特殊的类型,用于方法事件委托,比如你调用A类的methodA方法,其实背后是B类的methodA去执行。 + +印象中,要实现委托并不是一件非常自然直观的事情。但庆幸的是,Kotlin简化了这种语法,我们只需要通过by关键字就可以实现委托的效果。比如之前提过的by lazy语法,其实就是利用委托实现的延迟初始化语法。 + +```kotlin +val laziness: String by lazy { + println("I will hava a value") + "I an a lazy initialized string" +} +``` + +下面通过委托来替代多继承实现需求: + +```kotlin +interface CanFly { + fun fly() +} +interface CanEat { + fun eat() +} + +open class Flyer : CanFly { + override fun fly() { + println("I can fly") + } +} +open class Animal : CanEat { + override fun eat() { + println("I can eat") + } +} +class Bird(flyer: Flyer, animal: Animal) : CanFly by flyer, CanEat by animal {} + +fun main(args: Array) { + val flyer = Flyer() + val animal = Animal() + val b = Bird(flyer, animal) + b.fly() + b.eat() +} +``` + +有人可能会有疑问: 首先,委托方式怎么跟接口实现多继承如此相似,而且好像也并没有简单多少。其次,这种方式好像跟组合也很想,那么它到底有什么优势? 主要有以下两点: + +- 前面说到接口是无状态的,所以即使它提供了默认方法实现也是很简单的,不能实现复杂的逻辑,也不推荐在接口中实现复杂的方法逻辑。我们可以利用上面委托的这种方式,虽然它也是接口委托,但它是用一个具体的类去实现方法逻辑,可以拥有更强大的能力。 +- 假设我们需要继承的类是A,委托对象是B、C,我们再具体调用的时候并不是像组合一样A.B.method,而是可以直接调用A.method,这更能表达A拥有该method的能力,更加直观,虽然背后也是通过委托对象来执行具体的方法逻辑的。 + + + + + + + + + + + + + + + + + + + + +[上一篇:Kotlin学习教程(七)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%83).md) +[下一篇:Kotlin学习教程(八)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AB).md) + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/KotlinCourse/7.Kotlin_\346\263\250\350\247\243&\345\217\215\345\260\204&\346\211\251\345\261\225.md" "b/KotlinCourse/7.Kotlin_\346\263\250\350\247\243&\345\217\215\345\260\204&\346\211\251\345\261\225.md" new file mode 100644 index 00000000..fdbd19e2 --- /dev/null +++ "b/KotlinCourse/7.Kotlin_\346\263\250\350\247\243&\345\217\215\345\260\204&\346\211\251\345\261\225.md" @@ -0,0 +1,859 @@ +Kotlin学习教程(六) +=== + +### 注解 + +注解是将元数据附加到代码的方法。要声明注解,请将`annotation`修饰符放在类的前面: + +```kotlin +annotation class Fancy +``` + +注解的附加属性可以通过用元注解标注注解类来指定: + +- `@Target`指定可以用该注解标注的元素的可能的类型(类、函数、属性、表达式等) +- `@Retention`指定该注解是否存储在编译后的`class`文件中,以及它在运行时能否通过反射可见(默认都是`true`) +- `@Repeatable`允许在单个元素上多次使用相同的该注解 +- `@MustBeDocumented`指定该注解是公有`API`的一部分,并且应该包含在生成的`API`文档中显示的类或方法的签名中 + + +```kotlin +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, + AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.EXPRESSION) +@Retention(AnnotationRetention.SOURCE) +@MustBeDocumented +annotation class Fancy +``` + +### 用法 + +```kotlin +@Fancy class Foo { + @Fancy fun baz(@Fancy foo: Int): Int { + return (@Fancy 1) + } +} +``` + +如果需要对类的主构造函数进行标注,则需要在构造函数声明中添加`constructor`关键字,并将注解添加到其前面: + +```kotlin +class Foo @Inject constructor(dependency: MyDependency) { + // …… +} +``` + +### 反射 + +反射是这样的一组语言和库功能,它允许在运行时自省你的程序的结构。 +`Kotlin`让语言中的函数和属性做为一等公民、并对其自省(即在运行时获悉一个名称或者一个属性或函数的类型)与简单地使用函数式或响应式风格紧密相关。 + +在`Java`平台上,使用反射功能所需的运行时组件作为单独的`JAR`文件(`kotlin-reflect.jar`)分发。这样做是为了减少不使用反射功能的应用程序所需的 +运行时库的大小。如果你需要使用反射,请确保该`.jar`文件添加到项目的`classpath`中。 + + +### 类引用 + +最基本的反射功能是获取`Kotlin`类的运行时引用。要获取对静态已知的`Kotlin`类的引用,可以使用类字面值语法: + +```kotlin +val c = MyClass::class +``` +该引用是`KClass`类型的值。 +通过使用对象作为接收者,可以用相同的`::class`语法获取指定对象的类的引用: + +```kotlin +val widget: Widget = …… +assert(widget is GoodWidget) { "Bad widget: ${widget::class.qualifiedName}" } +``` + +当我们有一个命名函数声明如下: + +```kotlin +fun isOdd(x: Int) = x % 2 != 0 +``` +我们可以很容易地直接调用它`isOdd(5)`,但是我们也可以把它作为一个值传递。例如传给另一个函数。为此我们使用`::`操作符: + +```kotlin +val numbers = listOf(1, 2, 3) +println(numbers.filter(::isOdd)) // 输出 [1, 3] +``` + +### 扩展 + +扩展是`kotlin`中非常重要的一个特性,它能让我们对一些已有的类进行功能增加、简化,使他们更好的应对我们的需求。 + +```kotlin +// 对Context的扩展,增加了toast方法。为了更好的看到效果,我还加了一段log日志 +fun Context.toast(msg : String){ + Toast.makeText(this, msg, Toast.LENGTH_SHORT).show() + Log.d("text", "Toast msg : $msg") +} + +// Activity类,由于所有Activity都是Context的子类,所以可以直接使用扩展的toast方法 +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + ...... + toast("hello, Extension") + } +} + +// 输出 +Toast msg : hello, Extension +``` + +按照通常的做法,会写一个`ToastUtils`工具类,或者在`BaseActivity`中实现`toast`。但是使用扩展函数就会简单很。 +上面的例子就是在`Context`中添加新的方法,让我们以更简单的方式去显示`toast`,并且不用传入任何`context`参数,可以被任何`Context`或者 +它的子类调用。我们可以在任何地方(例如一个工具类文件中)声明这个函数,然后在`Activity`中将它作为普通方法来直接调用。当然了`Anko`中已经包括了 +自己的`toast`扩展函数。有关`Anko`后面会讲到。 + +`Kotlin`扩展函数允许我们在不改变已有类的情况下,为类添加新的函数。 +扩展函数是指对类的方法进行扩展,写法和定义方法类似,但是要声明目标类,也就是对哪个类进行扩展,`kotlin`中称之为`Top Level`。 +扩展函数表现得就像是属于这个类的一样,而且我们可以使用`this`关键字和调用所有`public`方法。 +扩展函数可以在已有类中添加新的方法,不会对原类做修改,扩展函数定义形式: +```kotlin +fun receiverType.functionName(params){ + body +} +receiverType:表示函数的接收者,也就是函数扩展的对象 +functionName:扩展函数的名称 +params:扩展函数的参数,可以为NULL +``` + +在上面我们举的扩展的例子就是扩展函数.其中`Context`就是目标类`Top Level`,我们把它放到方法名前,用点`.`表示从属关系。在方法体中用关键字 +`this`对本体进行调用。和普通方法一样,如果有返回值,在方法后面跟上返回类型,我这里没有返回值,所以直接省略了。 + +扩展函数的原理: 通过查看对应的Java代码可以发现: + +```kotlin +package com.st.stplayer.extension + +fun MutableList.exchange(fromIndex: Int, toIndex: Int) { + val tmp = this[fromIndex] + this[fromIndex] = this[toIndex] + this[toIndex] = tmp +} +``` + + + +```java +package com.st.stplayer.extension; + +import java.util.List; +import kotlin.Metadata; +import kotlin.jvm.internal.Intrinsics; +import org.jetbrains.annotations.NotNull; + +@Metadata( + mv = {1, 4, 2}, + bv = {1, 0, 3}, + k = 2, + d1 = {"\u0000\u0012\n\u0000\n\u0002\u0010\u0002\n\u0002\u0010!\n\u0002\u0010\b\n\u0002\b\u0003\u001a \u0010\u0000\u001a\u00020\u0001*\b\u0012\u0004\u0012\u00020\u00030\u00022\u0006\u0010\u0004\u001a\u00020\u00032\u0006\u0010\u0005\u001a\u00020\u0003¨\u0006\u0006"}, + d2 = {"exchange", "", "", "", "fromIndex", "toIndex", "stplayer_debug"} +) +public final class ExtenndsKt { + public static final void exchange(@NotNull List $this$exchange, int fromIndex, int toIndex) { + Intrinsics.checkNotNullParameter($this$exchange, "$this$exchange"); + int tmp = ((Number)$this$exchange.get(fromIndex)).intValue(); + $this$exchange.set(fromIndex, $this$exchange.get(toIndex)); + $this$exchange.set(toIndex, tmp); + } +} +``` + +通过上面的Java代码可以看出,我们可以将扩展函数理解为静态方法。而静态方法的特点是:它独立于该类的任何对象,且不依赖类的特定实例,被该类的所有实例共享。此外,被public修饰的静态方法本质上也就是全局方法。所以扩展函数不会带来额外的性能消耗。 + + + + + +##### 扩展函数的作用域 + +在平时写扩展时,我们通常会把扩展方法放到一个文件中,例如ViewExtends.kt: + +```kotlin +package com.charon.ext + +... +val View.isVisible: Boolean + get() = visibility == View.VISIBLE + +fun View.toBitmap(): Bitmap? { + clearFocus() + isPress = false + val willNotCache = willNotCacheDrawing() + setWillNotCacheDrawing(false) + // Reset the drawing cache background color to fully transparent + // for the duration of this operation + val color = drawingCacheBackgroundColor + drawingCacheBackgroundColor = 0 + if (color != 0) destroyDrawingCache() + buildDrawingCache() + val cacheBitmap = drawingCache + if (cacheBitmap == null) { + Log.e("Views", "failed to get bitmap from $this", RuntimeException()) + return null + } + val bitmap = Bitmap.createBitmap(cacheBitmap) + // Restore the view + destroyDrawingCache() + setWillNotCacheDrawing(willNotCache) + drawingCacheBackgroundColor = color + return bitmap +} +``` + +我们知道在同一个包内是可以直接调用exchange方法的。如果需要在其他包中调用,只需要import相应的方法即可,这与调用Java全局静态方法类似。除此之外,实际开发时我们也可能会将扩展函数定义在一个Class内部统一管理: + +```kotlin +class Extends { + fun MutableList.exchange(fromIndex: Int, toIndex: Int) { + val tmp = this[fromIndex] + this[fromIndex] = this[toIndex] + this[toIndex] = tmp + } +} +``` + +当扩展函数定义在Extends类内部时,情况就与之前不一样了,这个时候你会发现,之前的exchange方法无法调用了(之前调用位置在Extends类外部)。借助IDEA我们可以查看到它对应的Java代码,这里展示关键部分: + +```java +public static final class Extends { + public final void exchange(@NotNull list $receiver, int fromIndex, int toIndex) { + Intrinsics.checkParameterIsNotNull($receiver, "$receiver"); + int tmp = ((Number)$receiver.get(fromIndex)).intValue(); + $receiver.set(fromIndex, $receiver.get(toIndex)); + $receiver.set(toIndex, Integer.valueOf(tmp)); + } +} +``` + +我们看到,exchange方法上已经没有static关键字的修饰了。所以当扩展方法在一个Class内部时,我们只能在该类和该类的子类中进行调用。 + + + +##### 成员方法优先级总高于扩展函数 + +```kotlin +class Son { + fun foo() = println("son called member foo") +} +``` + +它包含一个成员方法foo(),加入我们哪天心血来潮,想对这个方法做特殊实现,利用扩展函数可能会写出如下代码: + +```kotlin +fun Son.foo() = println("son called extension foo") + +object Test { + @JvmStatic + fun main(args: Array) { + Son().foo() + } +} +``` + +在我们的预期中,我们希望调用的是扩展函数foo(),但是输出的结果为: son called member foo。这表明当扩展函数和现有类的成员方法同时存在时,Kotlin将会默认使用类的成员方法。看起来似乎不够合理,并且很容易引发一些问题:我定义了新的方法,为什么还是调用了旧的方法? + +但是换一个角度来思考,在多人开发的时候,如果每个人都对Son扩展了foo方法,是不是很容易造成混淆。对于第三方类库来说甚至是一场灾难:我们把不应该更改的方法改变了。所以在使用时,我们必须注意: 同名的类成员方法的优先级总高于扩展函数。 + + + +#### 被滥用的扩展函数 + +扩展函数在开发中为我们提供了非常多的便利,但是在实际应用中,我们可能会将这个特性滥用。例如ImageLoaderUtils这个加载网络图片为例: + +```kotlin +fun Context.loadImage(url: String, imageView: ImageView) { + GlideApp.with(this) + .load(url) + .placeholder(R.mipmap.img_default) + .error(R.mipmap.ic_error) + .into(imageView) +} + +// 在ImageActivity.kt中使用 +this.loadImage(url, imageView) +``` + +也许你在用的时候并没有感觉出什么奇怪的地方,但是实际上,我们并没有以任何方式扩展现有类。上述代码仅仅为了在函数调用的时候省去参数,这是一种滥用扩展机制的行为。 + +我们知道,Context作为"God Object",已经承担了很多责任。我们基于Context扩展,还很可能产生ImageView与传入上下文周期不一致导致的很多问题。 + +正确的做法应该是在ImageView上进行扩展: + +```kotlin +fun ImageView.loadImage(url: String) { + GlideApp.with(this.context) + .load(url) + .placeholder(R.mipmap.img_default) + .error(R.mipmap.ic_error) + .into(this) +} +``` + +这样在调用的时候,不仅省去了更多的参数,而且ImageView的声明周期也得到了保证。 + +在实际项目中,我们还需要考虑网络请求框架替换及维护的问题,一般会对图片的请求框架进行二次封装: + +```kotlin +object ImageLoader { + fun with(context: Context, url: String, imageView: ImageView) { + GlideApp.with(context) + .load(url) + .placeholder(R.mipmap.img_default) + .error(R.mipmap.ic_error) + .into(imageView) + } +} +``` + +所以,虽然扩展函数能够提供许多便利,我们还是应该注意在恰当的地方使用它,否则会造成不必要的麻烦。 + + + +##### 扩展属性 + + +扩展属性和扩展方法类似,是对目标类的属性进行扩展。扩展属性也会有`set`和`get`方法,并且要求实现这两个方法,不然会提示编译错误。 +因为扩展并不是在目标类上增加了这个属性,所以目标类其实是不持有这个属性的,我们通过`get`和`set`对这个属性进行读写操作的时候也不能使用 +`field`指代属性本体。可以使用`this`,依然表示的目标类。 + +```kotlin +// 扩展了一个属性paddingH +var View.panddingH : Int + get() = (paddingLeft + paddingRight) / 2 + set(value) { + setPadding(value, paddingTop, value, paddingBottom) + } + +// 设置值 +text.panddingH = 100 +``` + +给`View`扩展了一个属性`paddingH`,并给属性增加了`set`和`get`方法,然后可以在`activity`中通过`textview`调用。 + +扩展属性与扩展函数一样,其本质也是对应Java中的静态方法。由于扩展没有实际的将成员插入类中,因此对扩展属性来说幕后字段是无效的。 + + +##### 静态扩展 + +`kotlin`中的静态用关键字`companion`表示,但是它不是修饰属性或方法,而是定义一个方法块,在方法块中的所有方法和属性都是静态的, +这样就将静态部分统一包装了起来。静态部分的访问和`java`一致,直接使用类名+静态属性/方法名调用。 + +```kotlin +// 定义静态部分 +class Extension { + companion object{ + var name = "Extension" + } +} + +// 通过类名+属性名直接调用 +toast("hello, ${Extension.name}") + +// 输出 +Toast msg : hello, Extension +``` +上面例子中,`companion object`一起是修饰关键字,`part`是方法块的名称。其中方法块名称`part`可以省略,如果省略的话,默认缺省名为 +`Companion` + +静态的扩展和普通的扩展类似,但是在目标类要加上静态方法块的名称,所以如果我们要对一个静态部分扩展,就要先知道静态方法块的名称才行。 + +```kotlin +class Extension { + companion object part{ + var name = "Extension" + } +} + +// part为静态方法块名称 +fun Extension.part.upCase() : String{ + return name.toUpperCase() +} + +// 调用一下 +toast("hello, ${Extension.name}") +toast("hello, ${Extension.upCase()}") + +//输出 +Toast msg : hello, Extension +Toast msg : hello, EXTENSION +``` + + + +#### 标准库中的扩展函数 + +1. run + +先看一下run方法,它是利用扩展实现的,定义如下: + +```kotlin +public inline fun T.run(block: T.() -> R): R = block() +``` + +简单来说,run是任何类型T的通用扩展函数,run中执行了返回类型为R的扩展函数block,最终返回该扩展函数的结果。 + +在run函数中我们拥有一个单独的作用域,能够重新定义一个nickName变量,并且它的作用域只存在于run函数中: + +```kotlin +fun testFoo() { + val nickName = "111" + run { + val nickName = "xxx" + println(nickName) // xxx + } + println(nickName) // 111 +} +``` + +这个范围函数本身似乎不是很有用。但是相比范围,还有一点不错的是,它返回范围内最后一个对象。 + +例如现在有这么一个场景:用户点击领取新人奖励的按钮,如果用户此时没有登陆则弹出loginDialog,如果已经登录则弹出领取奖励的getNewAccountDialog。我们可以使用以下代码来处理这个逻辑: + +```kotlin +run { + if (!isLogin) loginDialog else getNewAccountDialog +}.show() +``` + + + +2. apply + +`apply`函数是这样的,调用某对象的`apply`函数,在函数范围内,可以任意调用该对象的任意方法,并返回该对象. + +```kotlin +fun testApply() { + ArrayList().apply { + add("testApply") + add("testApply") + add("testApply") + println("this = " + this) + }.let { println(it) } +} + +// 运行结果 +// this = [testApply, testApply, testApply] +// [testApply, testApply, testApply] +``` + +let函数和apply函数很像,唯一不同的是返回值。apply返回的是原来的对象,而let返回的是闭包里面的值。 + +3. let + +他的定义为: + +```kotlin +public inline fun T.let(block: (T) -> R): R = block(this) +``` + + + +如果对象的值不为空,则允许执行这个方法。返回值是函数里面最后一行,或者指定return,与run一样,它同样限制了变量的作用域。 + +```kotlin +private var test: String? = null + +private fun switchFragment(position: Int) { + test?.let { + LogUtil.e("@@@", "test is not null") + } +} +``` + +说到可能有人会觉得没什么用,用`if`判断下是不是空不就完了. + +```kotlin +private var test: String? = null + +private fun switchFragment(position: Int) { +// test?.let { +// LogUtil.e("@@@", "test is null") +// } + + if (test == null) { + LogUtil.e("@@@", "test is null") + } else { + LogUtil.e("@@@", "test is not null ${test}") + check(test) // 报错 + } +} +``` + +但是会报错:`Smart cast to 'String' is impossible, beacuase 'test' is a mutable property that could have been changed by this time` + + + +4. also + +also是Kotlin 1.1版本中新加入的内容,它像是let和apply函数的加强版。 + +```kotlin +public inline fun T.also(block: (T) -> Unit): T { block(this); return this } +``` + +与apply一致,它的返回值是该函数的接受者。 + +```kotlin +class Kot { + val student: Student? = getStu() + var age = 0 + fun dealStu() { + val result = student?.also { stu -> + this.age += stu.age + println(this.age) + println(stu.age) + this.age + } + } +} +``` + + + +#### `sNullOrEmpty | isNullOrBlank` + +```kotlin +public inline fun CharSequence?.isNullOrEmpty(): Boolean = this == null || this.length == 0 + +public inline fun CharSequence?.isNullOrBlank(): Boolean = this == null || this.isBlank() + +// If we do not care about the possibility of only spaces... +if (number.isNullOrEmpty()) { + // alert the user to fill in their number! +} + +// when we need to block the user from inputting only spaces +if (name.isNullOrBlank()) { + // alert the user to fill in their name! +} +``` + +#### `with`函数 + +with和apply这两个方法最大的作用就是可以让那个我们在写Lambda的时候,省略需要多次书写的对象名,默认用this关键字来指向它,this可以省略。 + +`with`是一个非常有用的函数,它包含在`Kotlin`的标准库中。它接收一个对象和一个扩展函数作为它的参数,然后使这个对象扩展这个函数。 +这表示所有我们在括号中编写的代码都是作为对象(第一个参数)的一个扩展函数,我们可以就像作为`this`一样使用所有它的`public`方法和属性。 +当我们针对同一个对象做很多操作的时候这个非常有利于简化代码。 + +```kotlin +fun testWith() { + with(ArrayList()) { + add("testWith") + add("testWith") + add("testWith") + println("this = " + this) + } +} +// 运行结果 +// this = [testWith, testWith, testWith] +``` + +#### `repeat`函数 + +`repeat`函数是一个单独的函数,定义如下: + +```kotlin +/** + * Executes the given function [action] specified number of [times]. + * + * A zero-based index of current iteration is passed as a parameter to [action]. + */ +@kotlin.internal.InlineOnly +public inline fun repeat(times: Int, action: (Int) -> Unit) { + contract { callsInPlace(action) } + + for (index in 0..times - 1) { + action(index) + } +} +``` + +通过代码很容易理解,就是循环执行多少次`block`中内容。 + +```kotlin +fun main(args: Array) { + repeat(3) { + println("Hello world") + } +} +``` + +运行结果是: + +```kotlin +Hello world +Hello world +Hello world +``` + +#### also + +also是kotlin 1.1版本中新加入的内容,它像是let和apply函数的加强版: + +```kotlin +public inline fun T.also(block: (T) -> Unit): T { + block(this); return this +} +``` + +与apply一致,它的返回值是该函数的接收者。 + +### 调度方式对扩展函数的影响 + +Kotlin是一种静态类型语言,我们创建的每个对象不仅具有运行时,还具有编译时类型。在使用扩展函数时,要清楚地了解静态和动态调度之间的区别。 + +#### 静态与动态调度 + +先用一个Java的例子: + +```java +class Base { + public void fun() { + System.out.println("I'm Base foo!"); + } +} +class Extended extends Base { + @Override + public void fun() { + System.out.println("I'm Extended foo!"); + } +} +Base base = new Extended(); +base.fun(); +``` + +毫无疑问,因为重写了fun方法,所以最终肯定会执行I'm Extended foo!。 + +变量base具有编译时类型Base和运行时类型Extended。当我们调用时,base.foo()将动态调度该方法,这意味着运行时类型(Extended)的方法被调用。 + +当我们调用重载方法时,调度变为静态并且仅取决于编译时类型。 + +```java +void foo(Base base) { + ... +} +void foo(Extended extended) { + ... +} +public static void main(String[] args) { + Base base = new Extended(); + foo(base); +} +``` + +在这种情况下,即使base本质上是Extended的实例,最终还是会执行Base的方法。 + + + +#### 扩展函数始终静态调度 + +可能你会好奇,这和扩展有什么关系?我们知道,扩展函数都有一个接收器(receiver),由于接收器实际上只是字节代码中编译方法的参数,因此你可以重载它。但不能覆盖它。这可能是成员和扩展函数之间最重要的区别:前者是动态调度的,后者总是静态调度的。 + +为了方便理解,举一个例子: + +```kotlin +open class Base +class Extended: Base() +fun Base.foo() = "I'm Base.foo!" +fun Extended.foo() = "I'm Extended.foo!" +fun main(args: Array) { + val instance: Base = Extended() + val instance2 = Extended() + println(instance.foo()) + println(instance2.foo()) +} +// 执行结果 +I'm Base.foo! +I'm Extended.foo! +``` + +由于只考虑了编译时类型,第一个打印将调用Base.foo(),而第二个打印将调用Extended.foo()。 + + + + + +## 用扩展函数封装Utils + +在Java中,我们习惯将常用的代码放到对应的工具类中,例如ToastUtils、NetworkUtils、ImageLoaderUtils等。以NetworkUtils为例,该类中我们通常会放入Android经常需要使用的网络相关方法。比如,我们现在有一个判断手机网络是否可用的方法: + +```java +public class NetworkUtils { + public static boolean isMobileConnected(Context context) { + if (context != null) { + ConnectivityManager mConnectivityManager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo mMobileNetworkInfo = mConnectivityManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE); + if (mMobilieNetworkInfo != null) { + return mMobileNetworkInfo.isAvailable(); + } + } + return false; + } +} +``` + +在需要调用的地方,我们通常会这样写: + +```java +boolean isConnected = NetworkUtils.isMobileConnected(context); +``` + +虽然用起来比没有封装之前优雅了很多,但是每次都要传入context,造成的烦琐我们先不计较,重要是可能会让调用者忽视context和mobileNetwork间的强关系。作为代码的使用者,我们更希望在调用时省略NetworkUtils类名,并且让isMobileConnected可以看起来像context的一个属性或方法。我们期望的是下面这样的使用方式: + +```java +boolean isConnected = context.isMobileConnected(); +``` + +由于Context是Android SDK自带的类,我们无法对其进行修改。在Java中目前只能通过继承Context新增静态成员方法来实现。但是在Kotlin中,我们通过扩展函数就能简单的实现: + +```kotlin +fun Context.isMobileConnected(): Boolean { + val mNetworkInfo = connectivityManager.activeNetworkInfo + if (mNetworkInfo != null) { + return mNetworkInfo.isAvailable + } + return false +} +``` + +我们只需要将以上代码放入对应文件中即可。这时我们已经摆脱了类的束缚,使用方式如下: + +```kotlin +val isConnected = context.isMobileConnected(); +``` + +值得一提的是,在Android中对Context的生命周期需要进行很好的把控。这里我们应该使用ApplicationContext,防止出现声明周期不一致导致的内存泄露或者其他问题。 + + + +## 运算符重载 + +  Kotlin允许我们对所有的运算符(+ - * / % ++ --),以及其他关键字进行重载,从而拓展这些运算符与关键字的用法。 + +语法:运算符重载使用的是operator关键字,在指定函数的前面加上operator关键字,就可以实现运算符重载了。 +指定函数,不同的运算符对应的重载函数是不同的,比如:+ 对应plus()  - 对应minus() +可以在类中,对同一运算符进行多次重载,来满足不同的需求。 + +运算符重载实际上是函数重载,本质上是对运算符函数的调用,从运算符到对应函数的映射过程由编译器完成。或者理解成是对已有的运算符赋予他们新的含义。重载的修饰符是operator。 + +举例: + +比如我们的+号,它的含义是两个数值相加: 1+1 = 2。 +号对应的函数名是plus。我们可以对+号这个函数进行重载,让它实现减法的效果。 + +下面我们实现一个Person数据类,然后重载Int的 + 号运算符,让一个Int对象可以直接和Person对象相加。返回的结果是这个Int对象减去Person对象的age的值。 + +```kotlin +data class Person(var name: String, var age: Int) +// 通过扩展的方式来实现运算符重载 +operator fun Int.plus(b: Person): Int { + return this - b.age +} +fun main() { + val person1 = Person("A", 3) + val testInt = 5 + println("result : ${testInt + person1}") //输出结果=2 +} +``` + +再比如,我们可以对Person数据类进行重载+号运算符,让Person对象可以直接调用+号来做一些函数操作。 + +```kotlin +data class Person(var name: String, var age: Int) { + operator fun plus(other: Person): Person { + return Person("${this.name} + ${other.name}", this.age + other.age) + } +} +fun main() { + val person1 = Person("A", 3) + val person2 = Person("B", 4) + val person3 = person1 + person2 + println("person3 = ${person3}") // person3 = Person(name=A + B, age=7) +} +``` + +一些场景运算符对应的函数名如下: + +### 一元前缀操作符 + +| 表达式 | 翻译为 | +| :----- | :--------------- | +| `+a` | `a.unaryPlus()` | +| `-a` | `a.unaryMinus()` | +| `!a` | `a.not()` | + + + +### 算术运算符 + +| 表达式 | 翻译为 | +| :------ | ---------------------------------- | +| `a + b` | `a.plus(b)` | +| `a - b` | `a.minus(b)` | +| `a * b` | `a.times(b)` | +| `a / b` | `a.div(b)` | +| `a % b` | `a.rem(b)`、 `a.mod(b)` (已弃用) | +| `a..b` | `a.rangeTo(b)` | + +### “In”操作符 + +| 表达式 | 翻译为 | +| :-------- | :--------------- | +| `a in b` | `b.contains(a)` | +| `a !in b` | `!b.contains(a)` | + + + +### 索引访问操作符 + +| 表达式 | 翻译为 | +| :-------------------- | :----------------------- | +| `a[i]` | `a.get(i)` | +| `a[i, j]` | `a.get(i, j)` | +| `a[i_1, ……, i_n]` | `a.get(i_1, ……, i_n)` | +| `a[i] = b` | `a.set(i, b)` | +| `a[i, j] = b` | `a.set(i, j, b)` | +| `a[i_1, ……, i_n] = b` | `a.set(i_1, ……, i_n, b)` | + +### 调用操作符 + +| 表达式 | 翻译为 | +| :---------------- | :----------------------- | +| `a()` | `a.invoke()` | +| `a(i)` | `a.invoke(i)` | +| `a(i, j)` | `a.invoke(i, j)` | +| `a(i_1, ……, i_n)` | `a.invoke(i_1, ……, i_n)` | + +### 相等与不等操作符 + +| 表达式 | 翻译为 | +| :------- | :-------------------------------- | +| `a == b` | `a?.equals(b) ?: (b === null)` | +| `a != b` | `!(a?.equals(b) ?: (b === null))` | + +### 比较操作符 + +| 表达式 | 翻译为 | +| :------- | :-------------------- | +| `a > b` | `a.compareTo(b) > 0` | +| `a < b` | `a.compareTo(b) < 0` | +| `a >= b` | `a.compareTo(b) >= 0` | +| `a <= b` | `a.compareTo(b) <= 0` | + + + + + +[上一篇:Kotlin学习教程(五)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%94).md) +[下一篇:Kotlin学习教程(七)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%83).md) + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! + diff --git "a/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" "b/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" new file mode 100644 index 00000000..d732bbaf --- /dev/null +++ "b/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" @@ -0,0 +1,97 @@ +8.Kotlin_协程 +=== + +Kotlin引入了协程(Coroutine)来支持更好的异步操作,利用它我们可以避免在异步编程中使用大量的回调,同时相比传统多线程技术,它更容易提升系统的高并发处理能力。 + +一些`API`启动长时间运行的操作(例如网络`IO`、文件`IO`、`CPU`或`GPU`密集型任务等),并要求调用者阻塞直到它们完成。协程提供了一种避免阻塞线程 +并用更廉价、更可控的操作替代线程阻塞的方法:协程挂起。 +协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、 +订阅相关事件、在不同线程(甚至不同机器!)上调度执行,而代码则保持如同顺序执行一样简单。 + + + +### 起源 + +协程是一个无优先级的子程序调用组件,允许子程序在特定的地方挂起恢复。线程包含于进程,协程包含于线程。只要内存足够,一个线程中可以有任意多个协程,但某一时刻只能有一个协程在运行,多个协程分享该线程分配到的计算机资源。 + +线程是由操作系统来进行调度的,当操作系统切换线程的时候,会产生一定的消耗。而协程不一样,协程是包含于线程的,也就是说协程是工作在线程之上的,协程的切换可以由程序自己来控制,不需要操作系统进行调度。这样的话就大大降低了开销。 + + + + +### 阻塞 vs 挂起 + +基本上,协程计算可以被挂起而无需阻塞线程。线程阻塞的代价通常是昂贵的,尤其在高负载时,因为只有相对少量线程实际可用,因此阻塞其中一个会导致一些 +重要的任务被延迟。 + +另一方面,协程挂起几乎是无代价的。不需要上下文切换或者`OS`的任何其他干预。最重要的是,挂起可以在很大程度上由用户库控制: +作为库的作者,我们可以决定挂起时发生什么并根据需求优化/记日志/截获。 + +另一个区别是,协程不能在随机的指令中挂起,而只能在所谓的挂起点挂起,这会调用特别标记的函数。 + +#### 挂起函数 + +当我们调用标记有特殊修饰符`suspend`的函数时,会发生挂起: + +```kotlin +suspend fun doSomething(foo: Foo): Bar { + …… +} +``` + +这样的函数称为挂起函数,因为调用它们可能挂起协程(如果相关调用的结果已经可用,库可以决定继续进行而不挂起)。挂起函数能够以与普通函数相同的方式 +获取参数和返回值,但它们只能从协程和其他挂起函数中调用。事实上,要启动协程, +必须至少有一个挂起函数,它通常是匿名的(即它是一个挂起`lambda`表达式)。让我们来看一个例子,一个简化的`async()`函数 +(源自`kotlinx.coroutines`库): + +```kotlin +fun async(block: suspend () -> T) +``` + +这里的`async()`是一个普通函数(不是挂起函数),但是它的`block`参数具有一个带`suspend`修饰符的函数类型:`suspend() -> T`。 +所以,当我们将一个`lambda`表达式传给`async()`时,它会是挂起`lambda`表达式,于是我们可以从中调用挂起函数: + +```kotlin +async { + doSomething(foo) + …… +} +``` + +继续该类比,`await()`可以是一个挂起函数(因此也可以在一个`async {}`块中调用),该函数挂起一个协程,直到一些计算完成并返回其结果: +```kotlin +async { + …… + val result = computation.await() + …… +} + +``` + +更多关于`async/await`函数实际在`kotlinx.coroutines`中如何工作的信息可以在这里找到。 + +请注意,挂起函数`await()`和`doSomething()`不能在像`main()`这样的普通函数中调用: +```kotlin +fun main(args: Array) { + doSomething() // 错误:挂起函数从非协程上下文调用 +} +``` + +还要注意的是,挂起函数可以是虚拟的,当覆盖它们时,必须指定`suspend`修饰符: +```kotlin +interface Base { + suspend fun foo() +} + +class Derived: Base { + override suspend fun foo() { …… } +} +``` + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\200).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\200).md" deleted file mode 100644 index 1d48ec61..00000000 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\200).md" +++ /dev/null @@ -1,534 +0,0 @@ -Kotlin学习教程(一) -=== - -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_kotlin.jpeg?raw=true) - -在`5月18`日谷歌在`I/O`开发者大会上宣布,将`Kotlin`语言作为安卓开发的一级编程语言。并且会在`Android Studio 3.0`版本全面支持`Kotlin`。 - -- `Kotlin`是一个基于`JVM`的新的编程语言,由[JetBrains](https://www.jetbrains.com/)开发。`JetBrains`作为目前广受欢迎的 -`Java IDE IntelliJ`的提供商,在`Apache`许可下已经开源其`Kotlin`编程语言。 -- `Kotlin`可以编译成`Java`字节码,也可以编译成`JavaScript`,方便在没有`JVM`的设备上运行。 -- `Kotlin`已正式成为`Android`官方开发语言。 - -[Kotlin官网](https://kotlinlang.org/) - -`JetBrains`这家公司非常牛逼,开发了很多著名的软件,他们在使用`Java`的过程中发现`java`比较笨重不方便,所以就开发了`kotlin`,`kotlin`是 -一种全栈的开发语言,可以用它进行开发`web`、`web`后端、`Android`等。 - -很多开发者都说`Google`学什么不好,非要学苹果,出个`android`的`swift`版本,一定会搞不起来没人用,所以不用浪费时间去学习。在这里想引用马云 -的一句话: -> 拥抱变化 - -`Google`做事,向来言出必行,之前在推行`Android Studio`时也是一片骂声,吐槽各种不好用,各种慢。但是现在`Android Studio`基本都已经普及了。 -我相信`Kotlin`也不会例外。所以我们不仅要学,还要要认真的学。 - - -### `Kotlin`的特性 - -- 它更加易表现:这是它最重要的优点之一。你可以编写少得多的代码。 -- `Kotlin`是一种兼容`Java`的语言 -- `Kotlin`比`Java`更安全,能够静态检测常见的陷阱。如:引用空指针 -- `Kotlin`比`Java`更简洁,通过支持`variable type inference,higher-order functions (closures),extension functions,mixins -and first-class delegation`等实现 -- `Kotlin`可与`Java`语言无缝通信。这意味着我们可以在`Kotlin`代码中使用任何已有的`Java`库;同样的`Kotlin`代码还可以为`Java`代码所用 -- `Kotlin`在代码中很少需要在代码中指定类型,因为编译器可以在绝大多数情况下推断出变量或是函数返回值的类型。这样就能获得两个好处:简洁与安全 - - -### `Kotlin`优势 - -- 全面支持`Lambda`表达式 -- 数据类`Data classes` -- 函数字面量和内联函数`Function literals & inline functions` -- 函数扩展`Extension functions` -- 空安全`Null safety` -- 智能转换`Smart casts` -- 字符串模板`String templates` -- 主构造函数`Primary constructors` -- 类委托`Class delegation` -- 类型推判`Type inference` -- 单例`Singletons` -- 声明点变量`Declaration-site variance` -- 区间表达式`Range expressions` - - -上面说简洁简洁,到底简洁在哪里?这里先用一个例子开始,在`Java`开发过程中经常会写一些`Bean`类: -```java -package com.charon.kotlinstudydemo; - -public class Person { - private int age; - private String name; - private float height; - private float weight; - - public int getAge() { - return age; - } - - public void setAge(int age) { - this.age = age; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public float getHeight() { - return height; - } - - public void setHeight(float height) { - this.height = height; - } - - public float getWeight() { - return weight; - } - - public void setWeight(float weight) { - this.weight = weight; - } - - @Override - public String toString() { - return "Person name is : " + name + " age is : " + age + " height is :" - + height + " weight is :" + weight; - } -} -``` -使用`Kotlin`: -```kotlin -package com.charon.kotlinstudydemo - -data class Person( - var name: String, - var age: Int, - var height: Float, - var weight: Float) -``` -这个数据类,它会自动生成所有属性和它们的访问器,以及一些有用的方法,比如`toString()`方法。 -这里插一嘴,从上面的例子中我们可以看到对于包的声明基本是一样的,唯一不同的是`kotlin`中后面结束不用分号。 - - -### 创建`Kotlin`项目 - -`Google`宣布在`Android Studio 3.0`版本会全面支持`Kotlin`,目前早就有预览版了 -[Android Studio Preview](https://developer.android.com/studio/preview/index.html)(个人感觉很好用,比2.3.3版本强多了)。 -直接通过`New Project`创建就可以,与创建普通`Java`项目唯一不同的是要勾选`Include Kotlin support`的选项。 - - -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/studio_create_kotlin.png?raw=true) - -创建完成后我们看一下`MainActivity`的代码: -```kotlin -// 定义包 -package com.charon.kotlinstudydemo - -// 导入 -import android.support.v7.app.AppCompatActivity -import android.os.Bundle - -// 定义类,继承AppCompatActivity -class MainActivity : AppCompatActivity() { - - // 重写方法用overide,函数名用fun声明 参数是a: 类型的形式 ?是啥?它是指明该对象可能为null, - // 如果有了?那在调用该方法的时候参数可以传递null进入,如果没有?传递null就会报错 - override fun onCreate(savedInstanceState: Bundle?) { - // super - super.onCreate(savedInstanceState) - // 调用方法 - setContentView(R.layout.activity_main) - } -} -``` - -我们就从`MainActivity`的代码开始介绍一些基本的语法。 - -### 变量 - -变量可以很简单地定义成可变`var`(可读可写)和不可变`val`(只读)的变量。 - -`val`与`Java`中使用的`final`很相似。一个不可变对象意味着它在实例化之后就不能再去改变它的状态了。如果你需要一个这个对象修改之后的版本, -那就会再创建一个新的对象。 - -声明: -```kotlin -var age: Int = 18 -val name: String = "charon" -``` - -再提示一下:`kotlin`中每行代码结束不需要分号了,不要和`java`是的每行都带分号 - -字面上可以写明具体的类型。这个不是必须的,但是一个通用的`Kotlin`实践时省略变量的类型我们可以让编译器自己去推断出具体的类型: -```kotlin -var age = 18 // int -val name = "charon" // string -var height = 180.5f // flat -var weight = 70.5 // double -``` - -在`Kotlin`中,一切都是对象。没有像`Java`中那样的原始基本类型。 -当然,像`Integer`,`Float`或者`Boolean`等类型仍然存在,但是它们全部都会作为对象存在的。基本类型的名字和它们工作方式都是与`Java`非常相似 -的,但是有一些不同之处你可能需要考虑到: - -- 数字类型中不会自动转型。举个例子,你不能给`Double`变量分配一个`Int`。必须要做一个明确的类型转换,可以使用众多的函数之一: - ```kotlin - private var age = 18 - private var weight = age.toFloat() - ``` -- 字符(`Char`)不能直接作为一个数字来处理。在需要时我们需要把他们转换为一个数字: - ```kotlin - val c: Char='c' - val i: Int = c.toInt() - ``` -- 位运算也有一点不同。在`Android`中,我们经常在`flags`中使用`或`: - ```java - // Java - int bitwiseOr = FLAG1 | FLAG2; - int bitwiseAnd = FLAG1 & FLAG2; - ``` - - ```kotlin - // Kotlin - val bitwiseOr = FLAG1 or FLAG2 - val bitwiseAnd = FLAG1 and FLAG2 - ``` - -- 一个`String`可以像数组那样访问,并且被迭代: - ```kotlin - var s = "charon" - var c = s[2] - - for (a in s) { - Log.e("@@@", a +""); - } - ``` - - -##### 编译期常量 - -已知值的属性可以使用`const`修饰符标记为编译期常量(类似`java`中的`public static final`)。 -`const`只能修复`val`不能修复`var`,这些属性需要满足以下要求: -- 位于顶层或者是`object`的一个成员 -- 用`String`或原生类型值初始化 -- 没有自定义`getter` - -```kotlin -// Const val are only allowed on top level or in objects -const val NAME: String = "charon" - -class MainActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - } -} -``` - - -##### 后端变量`Backing Fields`. - -在`kotlin`的`getter`和`setter`是不允许本身的局部变量的,因为属性的调用也是对`get`的调用,因此会产生递归,造成内存溢出。 - -例如: - -```kotlin -var count = 1 -var size: Int = 2 -set(value) { - Log.e("text", "count : ${count++}") - size = if (value > 10) 15 else 0 -} -``` -这个例子中就会内存溢出。 - -`kotlin`为此提供了一种我们要说的后端变量,也就是`field`。编译器会检查函数体,如果使用到了它,就会生成一个后端变量,否则就不会生成。 -我们在使用的时候,用`field`代替属性本身进行操作。 - - -##### 延迟初始化 - -我们说过,在类内声明的属性必须初始化,如果设置非`null`的属性,应该将此属性在构造器内进行初始化。 -假如想在类内声明一个`null`属性,在需要时再进行初始化(最典型的就是懒汉式单例模式),与`Kotlin`的规则是相背的,此时我们可以声明一个属性并 -延迟其初始化,此属性用`lateinit`修饰符修饰。 - -```kotlin -class MainActivity : AppCompatActivity() { - lateinit var name : String - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - var test = MainActivity() - // 要先调用方法让其初始化 - test.init() - // 再使用其属性 - Log.e("@@@", test.name) - } - - fun init() { - // 延迟初始化 - name = "charon" - } -} -``` -需要注意的是,我们在使用的时候,一定要确保属性是被初始化过的,通常先调用初始化方法,否则会有异常。 -如果只是用`lateinit`声明了,但是还没有调用初始化方法就使用,哪怕你判断了该变量是否为`null`也是会`crash`的。 -```kotlin -private lateinit var test: String - -private fun switchFragment(position: Int) { - if (test == null) { - LogUtil.e("@@@", "test is null") - } else { - LogUtil.e("@@@", "test is not null") - check(test) - } -} -``` -会报`kotlin.UninitializedPropertyAccessException: lateinit property test has not been initialized` - -除了使用`lateinit`外还可以使用`by lazy {}`效果是一样的: -```kotlin -private val test by lazy { "haha" } - -private fun switchFragment(position: Int) { - if (test == null) { - LogUtil.e("@@@", "test is null") - } else { - LogUtil.e("@@@", "test is not null ${test}") - check(test) - } -} -``` -执行结果: -``` -test is not null haha -``` - -那`lateinit`和`by lazy`有什么区别呢? - -- `by lazy{}`只能用在`val`类型而`lateinit`只能用在`var`类型 -- `lateinit`不能用在可空的属性上和`java`的基本类型上,否则会报`lateinit`错误 - - -### 类的定义:使用`class`关键字 - -类可以包含: -- 构造函数和初始化块 -- 函数 -- 属性 -- 嵌套类和内部类 -- 对象声明 - - -```kotlin -class MainActivity{ - -} -``` - -如果有参数的话你只需要在类名后面写上它的参数,如果这个类没有任何内容可以省略大括号: -```kotlin -class Person(name: String, age: Int) -``` - -##### 创建类的实例 - -```kotlin -val person = Person("charon", 18) -``` - -上面的类有一个默认的构造函数。 - -注意:创建类的实例不用`new`了啊。 - - -### 构造函数 - -在`Kotlin`中的一个类可以有一个主构造函数和一个或多个次构造函数。 - -##### 主构造函数 - -主构造函数是类头的一部分:它跟在类名(和可选的类型参数)后: -```kotlin -class Person constructor(name: String, surname: String) { -} -``` -如果主构造函数没有任何注解或者可见性修饰符,可以省略`constructor`关键字: -```kotlin -class Person(name: String, surname: String) { -} -``` - -主构造函数不能包含任何的代码。初始化的代码可以放到以`init`关键字作为前缀的初始化块中: - -```kotlin -class Person constructor(name: String, surname: String) { - init { - print("name is $name and surname is $surname") - } -} -``` - -如果构造函数有注解或可见性修饰符,那么`constructor`关键字是必需的,并且这些修饰符在它前面: -```kotlin -class Person private @Inject constructor(name: String, surname: String) { - init { - print("name is $name and surname is $surname") - } -} -``` - -##### 次构造函数 - -类也可以声明前缀有`constructor`的次构造函数: -```kotlin -class Person{ - constructor(name: String) { - print("name is $name") - } -} -``` - -如果类有一个主构造函数,每个次构造函数都需要委托给主构造函数(不然会报错), 可以直接委托或者通过别的次构造函数间接委托。 -委托到同一个类的另一个构造函数用`this`关键字即可: -```kotlin -class Person constructor(name: String) { - constructor(name: String, surName: String) : this(name) { - Log.d("@@@", "name is : $name surName is : $surName") - } -} -``` -使用该对象: -```kotlin -class MainActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - Person("charon", "chui") - } -} -``` -就会在`logcat`上打印: -`09-20 16:51:19.738 6010-6010/com.charon.kotlinstudydemo D/@@@: name is : charon surName is : chui` - -如果一个非抽象类没有声明任何(主或次)构造函数,它会有一个生成的不带参数的主构造函数。构造函数的可见性是`public`。 -如果你不希望你的类有一个公有构造函数,你需要声明一个带有非默认可见性的空的主构造函数: - -```kotlin -class Person private constructor(name: String) { -} -``` - - -### 接口:使用`interface`关键字 - -```kotlin -interface FlyingAnimal { - fun fly() -} -``` - -### 函数:通过`fun`关键字定义 - -```kotlin -fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) -} -``` -如果你没有指定它的返回值,它就会返回`Unit`与`Java`中的`void`类似,但是`Unit`是一个真正的对象。`Unit`可以省略, -你当然也可以指定任何其它的返回类型: -```kotlin -fun maxOf(a: Int, b: Int): Int { - if (a > b) { - return a - } else { - return b - } -} -``` - -然而如果返回的结果可以使用一个表达式计算出来,你可以不使用括号而是使用等号: -```kotlin -fun add(x: Int,y: Int) : Int = x + y -``` - -我们可以给参数指定一个默认值使得它们变得可选,这是非常有帮助的。这里有一个例子,在`Activity`中创建了一个函数用来`Toast`一段信息: -```kotlin -fun toast(message: String, length: Int = Toast.LENGTH_SHORT) { - Toast.makeText(this, message, length).show() -} -``` -上面代码中第二个参数`length`指定了一个默认值。这意味着你调用的时候可以传入第二个值或者不传,这样可以避免你需要的重载函数: - -```kotlin -toast("Hello") -toast("Hello", Toast.LENGTH_LONG) -``` - -##### 自定义`get set`方法: - -`Kotlin`会默认创建`set get`方法,我们也可以自定义`get set`方法: -`kotlin`预留了一个在`set`和`get`中访问的变量`field`关键字: - -```kotlin -class Person constructor() { - var name: String = "" - get() = field - set(value) { - field = "$value" - } - - var age: Int = 0 - get() = field - set(value) { - field = value - } -} -``` -按照惯例`set`参数的名称是`value`,但是如果你喜欢你可以选择一个不同的名称。 - - -##### 可变长参数函数:使用`vararg`关键字 - -```kotlin -fun vars(vararg v:Int){ - for(vt in v){ - print(vt) - } -} - -// 测试 -fun main(args: Array) { - vars(1,2,3,4,5) // 输出12345 -} -``` - - -### 注释 - -和`Java`差不多 - -```kotlin - -// 这是一个行注释 - -/* 这是一个多行的 - 块注释。 */ -``` - - -[下一篇:Kotlin学习教程(二)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%8C).md) - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! - diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\203).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\203).md" deleted file mode 100644 index face6efd..00000000 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\203).md" +++ /dev/null @@ -1,96 +0,0 @@ -Kotlin学习教程(七) -=== - -这篇文章主要学习下`lambda`表达式。因为后续一些例子会用到。 - - -> “Lambda 表达式”(lambda expression)其实就是匿名函数,`Lambda`表达式基于数学中的`λ`演算得名,直接对应于其中的`lambda`抽象 -> `(lambda abstraction)`,是一个匿名函数,即没有函数名的函数。`Lambda`表达式可以表示闭包(注意和数学传统意义上的不同)。 - -`Java 8`的一个大亮点是引入`Lambda`表达式,使用它设计的代码会更加简洁。 - -```java -// 没有使用Lambda的老方法: -button.addActionListener(new ActionListener(){ - public void actionPerformed(ActionEvent ae){ - System.out.println("Actiondetected"); - } -}); -// 使用Lambda: -button.addActionListener(()->{ - System.out.println("Actiondetected"); -}); - - -// 不采用Lambda的老方法: -Runnable runnable1=new Runnable(){ - @Override - public void run(){ - System.out.println("RunningwithoutLambda"); - } -}; -// 使用Lambda: -Runnable runnable2=()->{ - System.out.println("RunningfromLambda"); -}; -``` - -`Lambda`能让代码更简洁,而主打简洁的`Kotlin`怎么可能不支持呢? 当然会支持。 - -下面来看看一个简短的概述: - -- `lambda`表达式总是被大括号括着 -- 其参数(如果有的话)在`->`之前声明(参数类型可以省略), -- 函数体(如果存在的话)在`->`后面。 - -`Lambda`表达式是定义匿名函数的简单方法。由于`Lambda`表达式避免在抽象类或接口中编写明确的函数声明,进而也避免了类的实现部分, -所以它是非常有用的。在`Kotlin`语言中,可以将一函数作为另一函数的参数。 - -`Lambda`表达式由箭头左侧函数的参数(在圆括号里的内容)定义的,将值返回到箭头右侧。 -`view.setOnClickListener({ view -> toast("Click")})` -在定义函数时,必须在箭头的左侧用方括号,并指定参数值,而函数的执行代码在箭头右侧。如果左侧不使用参数,甚至可以省去左侧部分: -`view.setOnClickListener({ toast("Click") })` -如果函数的最后一个参数是一个函数的话,可以将作为参数的函数移到圆括号外面: -`view.setOnClickListener() { toast("Click") }` - - -先看一个例子: - -```kotlin -fun compare(a: String, b: String): Boolean { - return a.length < b.length -} -max(strings, compare) -``` -就是找出`strings`里面最长的那个。但是我个人觉得`compare`还是很碍眼的,因为我并不想在后面引用他,那我怎么办呢,就是用“匿名函数”方式。 -```kotlin -max(strings, (a,b)->{a.length < b.length}) -``` - -`(a,b)->{a.length < b.length}`就是一个没有名字的函数,直接作为参数赋给`max`方法的第二个参数。但这个方法有很多东西都没有写明,如: - -- 参数的类型 -- 返回值的类型 - -但这些真的必要吗?`a.length < b.length`很明显返回一个`Boolean`的值,再就是`max`的定义中肯定也定义了这个函数的参数类型和返回值类型。 -这么明显的事为什么不让计算机自己去做而要让人写代码去做呢?这就是匿名函数的好处了。到这里,我们已经和`Lambda`很接近了。 - -```kotlin -val sum: (Int, Int) -> Int = { x, y -> x + y } -``` - -`Lambda`表达式就是被大括号括着的那一部分,在`->`符号之前有参数声明,函数体跟在一个`->`符号之后。 -而且此`Lambda`表达式之前有一个匿名的函数声明(在此例中两个`Int`型的输入,一个`Int`型的返回值),这个声明是可以不使用的。 -则此`Lambda`表达式变成`val sum = { x: Int, y: Int -> x + y }`,此时`Lambda`表达式会根据主体中的最后一个(或可能是单个)表达式会视为 -返回值。当然,在某些特定情况下,`x`、`y`的类型了是可以推断的,所以`val sum = { x, y -> x + y }`。 - - - -[上一篇:Kotlin学习教程(六)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AD).md) -[下一篇:Kotlin学习教程(八)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AB).md) - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\211).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\211).md" deleted file mode 100644 index cb0eb4e2..00000000 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\211).md" +++ /dev/null @@ -1,196 +0,0 @@ -Kotlin学习教程(三) -=== - -前面介绍了基本语法和编码规范后,接下来学习下基本类型。 - -在`Kotlin`中,所有东西都是对象,在这个意义上讲我们可以在任何变量上调用成员函数和属性。 一些类型可以有特殊的内部表示——例如, -数字、字符和布尔值可以在运行时表示为原生类型值,但是对于用户来说,它们看起来就像普通的类。 在本节中,我们会描述`Kotlin`中使用的基本类型: -数字、字符、布尔值、数组与字符串。 - -### 数字 - -`Kotlin`处理数字在某种程度上接近`Java`,但是并不完全相同。例如,对于数字没有隐式拓宽转换(如`Java`中`int`可以隐式转换为`long`), -另外有些情况的字面值略有不同。 - -`Kotlin`提供了如下的内置类型来表示数字: - -``` -Type Bit width -Double 64 -Float 32 -Long 64 -Int 32 -Short 16 -Byte 8 -```` - -注意在`Kotlin`中字符不是数字,字符用`Char`类型表示。它们不能直接当作数字 - - -### 字面常量 - -数值常量字面值有以下几种: -- 十进制:123 -- `Long`类型用大写`L`标记:`123L` -- 十六进制:`0x0F` -- 二进制:`0b00001011` - -注意: 不支持八进制 - -`Kotlin`同样支持浮点数的常规表示方法: - -默认`double`:123.5、123.5e10,`Float`用`f`或者`F`标记:`123.5f` - -你可以使用下划线使数字常量更易读 - -```kotlin -val oneMillion = 1_000_000 -val creditCardNumber = 1234_5678_9012_3456L -val socialSecurityNumber = 999_99_9999L -val hexBytes = 0xFF_EC_DE_5E -val bytes = 0b11010010_01101001_10010100_10010010 -``` - -### 引用相等 - -引用相等由`===`以及其否定形式`!===`操作判断。`a === b`当且仅当`a`和`b`指向同一个对象时求值为`true`。 - -### 结构相等 - -结构相等由`==`以及其否定形式`!==`操作判断。按照惯例,像`a == b`这样的表达式会翻译成 -`a?.equals(b) ?: (b === null)` -也就是说如果`a`不是`null`则调用`equals(Any?)`函数,否则即`a`是`null`检查`b`是否与`null`引用相等。 - -```kotlin -val a: Int = 10000 -print(a === a) // 输出“true” -val boxedA: Int? = agaomnh -val anotherBoxedA: Int? = a -print(boxedA === anotherBoxedA) // !!!输出“false”!!! -``` - -另一方面,它保留了相等性: -```kotlin -val a: Int = 10000 -print(a == a) // 输出“true” -val boxedA: Int? = a -val anotherBoxedA: Int? = a -print(boxedA == anotherBoxedA) // 输出“true” -``` - -### 显式转换 - -由于不同的表示方式,较小类型并不是较大类型的子类型。 如果它们是的话,就会出现下述问题: - -```kotlin -// 假想的代码,实际上并不能编译: -val a: Int? = 1 // 一个装箱的 Int (java.lang.Integer) -val b: Long? = a // 隐式转换产生一个装箱的 Long (java.lang.Long) -print(a == b) // 惊!这将输出“false”鉴于 Long 的 equals() 检测其他部分也是 Long -``` - -所以同一性还有相等性都会在所有地方悄无声息地失去。 -因此较小的类型不能隐式转换为较大的类型。 这意味着在不进行显式转换的情况下我们不能把`Byte`型值赋给一个`Int`变量。 - -```kotlin -val b: Byte = 1 // OK, 字面值是静态检测的 -val i: Int = b // 错误 -``` - -我们可以显式转换来拓宽数字 -```kotlin -val i: Int = b.toInt() // OK: 显式拓宽 -``` - -每个数字类型支持如下的转换: - -```kotlin -toByte(): Byte -toShort(): Short -toInt(): Int -toLong(): Long -toFloat(): Float -toDouble(): Double -toChar(): Char -``` - -### 运算 - -这是完整的位运算列表(只用于`Int`和`Long`): - -```kotlin -shl(bits) – 有符号左移 (Java 的 <<) -shr(bits) – 有符号右移 (Java 的 >>) -ushr(bits) – 无符号右移 (Java 的 >>>) -and(bits) – 位与 -or(bits) – 位或 -xor(bits) – 位异或 -inv() – 位非 -相等性检测:a == b 与 a != b -比较操作符:a < b、 a > b、 a <= b、 a >= b -区间实例以及区间检测:a..b、 x in a..b、 x !in a..b -|| – 短路逻辑或 -&& – 短路逻辑与 -! - 逻辑非 -``` - - -### 字符串 - -字符串用`String`类型表示。字符串是不可变的。字符串的元素——字符可以使用索引运算符访问:`s[i]`。可以用`for`循环迭代字符串: - -```kotlin -for (c in str) { - println(c) -} -``` -`Kotlin`有两种类型的字符串字面值: 转义字符串可以有转义字符,以及原生字符串可以包含换行和任意文本。转义字符串很像`Java`字符串: -```kotlin -val s = "Hello, world!\n" -``` -转义采用传统的反斜杠方式。 - -原生字符串 使用三个引号`"""`分界符括起来,内部没有转义并且可以包含换行和任何其他字符: - -```kotlin -val text = """ - for (c in "foo") - print(c) -""" -``` -你可以通过`trimMargin()`函数去除前导空格: - -```kotlin -val text = """ - |Tell me and I forget. - |Teach me and I remember. - |Involve me and I learn. - |(Benjamin Franklin) - """.trimMargin() -``` - -### 字符串模板 - -字符串可以包含模板表达式,即一些小段代码,会求值并把结果合并到字符串中。模板表达式以美元符`$`开头,由一个简单的名字构成: - -```kotlin -val i = 10 -val s = "i = $i" // 求值结果为 "i = 10" -``` - -或者用花括号括起来的任意表达式: -```kotlin -val s = "abc" -val str = "$s.length is ${s.length}" // 求值结果为 "abc.length is 3" -``` - - -[上一篇:Kotlin学习教程(二)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%8C).md) -[下一篇:Kotlin学习教程(四)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%9B%9B).md) - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! - diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\271\235).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\271\235).md" index 9d2b1b1c..1a484128 100644 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\271\235).md" +++ "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\271\235).md" @@ -170,6 +170,14 @@ class MyActivity : Activity() { ``` `textView`是对`Activity`的一项扩展属性,与在`activity_main.xml`中的声明具有同样类型。 +这里,你肯定会想,虽然省略了R.id.几个字符,但是引入是否会造成性能问题? 值得引入、使用 kotlin-android-extensions吗?如果我们对其反编译,就可以看到对应Java代码的实现,在第一次使用空间的时候,会在缓存集合中进行查找,有就直接使用,没有就通过findViewById进行查找,并添加到缓存的集合中。其还提供了$clearFindViewByIdCache()方法用于清除缓存,在我们想要彻底替换界面控件时可以使用。 + +在Fragment的onDestroyView()方法中默认调用了$clearFindViewByIdCache()清除缓存,而Activity没有。 + +所以我们并没有完全离开findViewById,只是Kotlin的扩展插件利用缓存的方式让我们开发更方便、更快捷。 + + + ### 网络请求 diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\272\214).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\272\214).md" deleted file mode 100644 index a4d12dad..00000000 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\272\214).md" +++ /dev/null @@ -1,88 +0,0 @@ -Kotlin学习教程(二) -=== - -上一篇文章介绍了`Kotlin`的基本语法,我感觉在继续学习更多知识之前有必要单独介绍以下编码规范。 - -不管学什么东西,开始形成的习惯以后想改都比较困难。所以开始就用规范的方式学习是最好的。 - - -### 命名风格 - -如果拿不准的时候,默认使用`Java`的编码规范,比如: - -- 使用驼峰法命名(并避免命名含有下划线) -- 类型名以大写字母开头 -- 方法和属性以小写字母开头 -- 使用4个空格缩进 -- 公有函数应撰写函数文档,这样这些文档才会出现在`Kotlin Doc`中 - - -### 冒号 - -类型和超类型之间的冒号前要有一个空格,而实例和类型之间的冒号前不要有空格: - -```kotlin -interface Foo : Bar { - fun foo(a: Int): T -} -``` - -### `Lambda`表达式 - -在`lambda`表达式中, 大括号左右要加空格,分隔参数与代码体的箭头左右也要加空格。`lambda`表达应尽可能不要写在圆括号中: - -```kotlin -list.filter { it > 10 }.map { element -> element * 2 } -``` - -### 类头格式化 - -有少数几个参数的类可以写成一行: - -```kotlin -class Person(id: Int, name: String) -``` - -具有较长类头的类应该格式化,以使每个主构造函数参数位于带有缩进的单独一行中。 此外,右括号应该另起一行。如果我们使用继承, -那么超类构造函数调用或者实现接口列表应位于与括号相同的行上: - -```kotlin -class Person( - id: Int, - name: String, - surname: String -) : Human(id, name) { - // …… -} -``` -对于多个接口,应首先放置超类构造函数调用,然后每个接口应位于不同的行中: -```kotlin -class Person( - id: Int, - name: String, - surname: String -) : Human(id, name), - KotlinMaker { - // …… -} -``` - - -### `Unit` - -如果函数返回`Unit`类型,该返回类型应该省略: -```kotlin -fun foo() { // 省略了 ": Unit" - -} -``` - - -[上一篇:Kotlin学习教程(一)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%80).md) -[下一篇:Kotlin学习教程(三)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%89).md) - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! - diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\253).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\253).md" index ee364cba..6ed5d235 100644 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\253).md" +++ "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\253).md" @@ -4,6 +4,8 @@ Kotlin学习教程(八) `Kotlin`协程 --- +Kotlin引入了协程(Coroutine)来支持更好的异步操作,利用它我们可以避免在异步编程中使用大量的回调,同时相比传统多线程技术,它 + 一些`API`启动长时间运行的操作(例如网络`IO`、文件`IO`、`CPU`或`GPU`密集型任务等),并要求调用者阻塞直到它们完成。协程提供了一种避免阻塞线程 并用更廉价、更可控的操作替代线程阻塞的方法:协程挂起。 协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、 @@ -421,130 +423,31 @@ class Group(val name: String) { -常用操作符及函数 ---- +### Any -#### `let`操作符 +我们都知道,Java并不能在真正意义上被称为一门“ 纯面向对象”语言,因为它的原始类型(如int)的值与函数等并不能被视作对象。 -如果对象的值不为空,则允许执行这个方法。返回值是函数里面最后一行,或者指定`return` -```kotlin -private var test: String? = null - -private fun switchFragment(position: Int) { - test?.let { - LogUtil.e("@@@", "test is not null") - } -} -``` +但是Kotlin不同,在Kotlin的类型系统中,并不区分原始类型(基本数据类型)和包装类型,我们使用的始终是同一个类型。虽然从严格意义上,我们不能说Kotlin是一门纯面向对象的语言,但它显然比Java有更纯的设计。 -说到可能有人会觉得没什么用,用`if`判断下是不是空不就完了. -```kotlin -private var test: String? = null - -private fun switchFragment(position: Int) { -// test?.let { -// LogUtil.e("@@@", "test is null") -// } - - if (test == null) { - LogUtil.e("@@@", "test is null") - } else { - LogUtil.e("@@@", "test is not null ${test}") - check(test) // 报错 - } -} -``` -但是会报错:`Smart cast to 'String' is impossible, beacuase 'test' is a mutable property that could have been changed by this time` - -#### `sNullOrEmpty | isNullOrBlank` - -```kotlin -public inline fun CharSequence?.isNullOrEmpty(): Boolean = this == null || this.length == 0 - -public inline fun CharSequence?.isNullOrBlank(): Boolean = this == null || this.isBlank() - -// If we do not care about the possibility of only spaces... -if (number.isNullOrEmpty()) { - // alert the user to fill in their number! -} - -// when we need to block the user from inputting only spaces -if (name.isNullOrBlank()) { - // alert the user to fill in their name! -} -``` -#### `with`函数 -`with`是一个非常有用的函数,它包含在`Kotlin`的标准库中。它接收一个对象和一个扩展函数作为它的参数,然后使这个对象扩展这个函数。 -这表示所有我们在括号中编写的代码都是作为对象(第一个参数)的一个扩展函数,我们可以就像作为`this`一样使用所有它的`public`方法和属性。 -当我们针对同一个对象做很多操作的时候这个非常有利于简化代码。 +#### Any:非空类型的跟类型 -```kotlin -fun testWith() { - with(ArrayList()) { - add("testWith") - add("testWith") - add("testWith") - println("this = " + this) - } -} -// 运行结果 -// this = [testWith, testWith, testWith] -``` +与Object作为Java类层级结构的顶层类似,Any类型是Kotlin中所有非空类型(如String、Int)的超类,如: -#### `repeat`函数 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/kotlin_any.png?raw=true) -`repeat`函数是一个单独的函数,定义如下: -```kotlin -/** - * Executes the given function [action] specified number of [times]. - * - * A zero-based index of current iteration is passed as a parameter to [action]. - */ -@kotlin.internal.InlineOnly -public inline fun repeat(times: Int, action: (Int) -> Unit) { - contract { callsInPlace(action) } +与Java不同的是,Kotlin不区分“原始类型”(primitive type)和其他的类型,他们都是同一类型层级结构的一部分。 如果定义了一个没有指定父类型的类型,则该类型将是Any的直接子类型。如: - for (index in 0..times - 1) { - action(index) - } -} -``` -通过代码很容易理解,就是循环执行多少次`block`中内容。 -```kotlin -fun main(args: Array) { - repeat(3) { - println("Hello world") - } -} -``` -运行结果是: ```kotlin -Hello world -Hello world -Hello world +class Animal(val weight: Double) ``` -#### `apply`函数 +#### Any?:所有类型的根类型 -`apply`函数是这样的,调用某对象的`apply`函数,在函数范围内,可以任意调用该对象的任意方法,并返回该对象 -```kotlin -fun testApply() { - ArrayList().apply { - add("testApply") - add("testApply") - add("testApply") - println("this = " + this) - }.let { println(it) } -} +如果说Any是所有非空类型的根类型,那么Any?才是所有类型(可空和非空类型)的根类型。这也就是说?Any?是?Any的父类型。 -// 运行结果 -// this = [testApply, testApply, testApply] -// [testApply, testApply, testApply] -``` -`run`函数和`apply`函数很像,只不过run函数是使用最后一行的返回,apply返回当前自己的对象。 [上一篇:Kotlin学习教程(七)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%83).md) diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\255).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\255).md" deleted file mode 100644 index f672121e..00000000 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\255).md" +++ /dev/null @@ -1,204 +0,0 @@ -Kotlin学习教程(六) -=== - -### 注解 - -注解是将元数据附加到代码的方法。要声明注解,请将`annotation`修饰符放在类的前面: - -```kotlin -annotation class Fancy -``` - -注解的附加属性可以通过用元注解标注注解类来指定: - -- `@Target`指定可以用该注解标注的元素的可能的类型(类、函数、属性、表达式等) -- `@Retention`指定该注解是否存储在编译后的`class`文件中,以及它在运行时能否通过反射可见(默认都是`true`) -- `@Repeatable`允许在单个元素上多次使用相同的该注解 -- `@MustBeDocumented`指定该注解是公有`API`的一部分,并且应该包含在生成的`API`文档中显示的类或方法的签名中 - - -```kotlin -@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, - AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.EXPRESSION) -@Retention(AnnotationRetention.SOURCE) -@MustBeDocumented -annotation class Fancy -``` - -### 用法 - -```kotlin -@Fancy class Foo { - @Fancy fun baz(@Fancy foo: Int): Int { - return (@Fancy 1) - } -} -``` - -如果需要对类的主构造函数进行标注,则需要在构造函数声明中添加`constructor`关键字,并将注解添加到其前面: - -```kotlin -class Foo @Inject constructor(dependency: MyDependency) { - // …… -} -``` - -### 反射 - -反射是这样的一组语言和库功能,它允许在运行时自省你的程序的结构。 -`Kotlin`让语言中的函数和属性做为一等公民、并对其自省(即在运行时获悉一个名称或者一个属性或函数的类型)与简单地使用函数式或响应式风格紧密相关。 - -在`Java`平台上,使用反射功能所需的运行时组件作为单独的`JAR`文件(`kotlin-reflect.jar`)分发。这样做是为了减少不使用反射功能的应用程序所需的 -运行时库的大小。如果你需要使用反射,请确保该`.jar`文件添加到项目的`classpath`中。 - - -### 类引用 - -最基本的反射功能是获取`Kotlin`类的运行时引用。要获取对静态已知的`Kotlin`类的引用,可以使用类字面值语法: - -```kotlin -val c = MyClass::class -``` -该引用是`KClass`类型的值。 -通过使用对象作为接收者,可以用相同的`::class`语法获取指定对象的类的引用: - -```kotlin -val widget: Widget = …… -assert(widget is GoodWidget) { "Bad widget: ${widget::class.qualifiedName}" } -``` - -当我们有一个命名函数声明如下: - -```kotlin -fun isOdd(x: Int) = x % 2 != 0 -``` -我们可以很容易地直接调用它`isOdd(5)`,但是我们也可以把它作为一个值传递。例如传给另一个函数。为此我们使用`::`操作符: - -```kotlin -val numbers = listOf(1, 2, 3) -println(numbers.filter(::isOdd)) // 输出 [1, 3] -``` - -### 扩展 - -扩展是`kotlin`中非常重要的一个特性,它能让我们对一些已有的类进行功能增加、简化,使他们更好的应对我们的需求。 - -```kotlin -// 对Context的扩展,增加了toast方法。为了更好的看到效果,我还加了一段log日志 -fun Context.toast(msg : String){ - Toast.makeText(this, msg, Toast.LENGTH_SHORT).show() - Log.d("text", "Toast msg : $msg") -} - -// Activity类,由于所有Activity都是Context的子类,所以可以直接使用扩展的toast方法 -class MainActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - ...... - toast("hello, Extension") - } -} - -// 输出 -Toast msg : hello, Extension -``` - -按照通常的做法,会写一个`ToastUtils`工具类,或者在`BaseActivity`中实现`toast`。但是使用扩展函数就会简单很。 -上面的例子就是在`Context`中添加新的方法,让我们以更简单的方式去显示`toast`,并且不用传入任何`context`参数,可以被任何`Context`或者 -它的子类调用。我们可以在任何地方(例如一个工具类文件中)声明这个函数,然后在`Activity`中将它作为普通方法来直接调用。当然了`Anko`中已经包括了 -自己的`toast`扩展函数。有关`Anko`后面会讲到。 - -`Kotlin`扩展函数允许我们在不改变已有类的情况下,为类添加新的函数。 -扩展函数是指对类的方法进行扩展,写法和定义方法类似,但是要声明目标类,也就是对哪个类进行扩展,`kotlin`中称之为`Top Level`。 -扩展函数表现得就像是属于这个类的一样,而且我们可以使用`this`关键字和调用所有`public`方法。 -扩展函数可以在已有类中添加新的方法,不会对原类做修改,扩展函数定义形式: -```kotlin -fun receiverType.functionName(params){ - body -} -receiverType:表示函数的接收者,也就是函数扩展的对象 -functionName:扩展函数的名称 -params:扩展函数的参数,可以为NULL -``` - -在上面我们举的扩展的例子就是扩展函数.其中`Context`就是目标类`Top Level`,我们把它放到方法名前,用点`.`表示从属关系。在方法体中用关键字 -`this`对本体进行调用。和普通方法一样,如果有返回值,在方法后面跟上返回类型,我这里没有返回值,所以直接省略了。 - - -##### 扩展属性 - - -扩展属性和扩展方法类似,是对目标类的属性进行扩展。扩展属性也会有`set`和`get`方法,并且要求实现这两个方法,不然会提示编译错误。 -因为扩展并不是在目标类上增加了这个属性,所以目标类其实是不持有这个属性的,我们通过`get`和`set`对这个属性进行读写操作的时候也不能使用 -`field`指代属性本体。可以使用`this`,依然表示的目标类。 - -```kotlin -// 扩展了一个属性paddingH -var View.panddingH : Int - get() = (paddingLeft + paddingRight) / 2 - set(value) { - setPadding(value, paddingTop, value, paddingBottom) - } - -// 设置值 -text.panddingH = 100 -``` - -给`View`扩展了一个属性`paddingH`,并给属性增加了`set`和`get`方法,然后可以在`activity`中通过`textview`调用。 - - -##### 静态扩展 - -`kotlin`中的静态用关键字`companion`表示,但是它不是修饰属性或方法,而是定义一个方法块,在方法块中的所有方法和属性都是静态的, -这样就将静态部分统一包装了起来。静态部分的访问和`java`一致,直接使用类名+静态属性/方法名调用。 - -```kotlin -// 定义静态部分 -class Extension { - companion object part{ - var name = "Extension" - } -} - -// 通过类名+属性名直接调用 -toast("hello, ${Extension.name}") - -// 输出 -Toast msg : hello, Extension -``` -上面例子中,`companion object`一起是修饰关键字,`part`是方法块的名称。其中方法块名称`part`可以省略,如果省略的话,默认缺省名为 -`Companion` - -静态的扩展和普通的扩展类似,但是在目标类要加上静态方法块的名称,所以如果我们要对一个静态部分扩展,就要先知道静态方法块的名称才行。 - -```kotlin -class Extension { - companion object part{ - var name = "Extension" - } -} - -// part为静态方法块名称 -fun Extension.part.upCase() : String{ - return name.toUpperCase() -} - -// 调用一下 -toast("hello, ${Extension.name}") -toast("hello, ${Extension.upCase()}") - -//输出 -Toast msg : hello, Extension -Toast msg : hello, EXTENSION -``` - - - -[上一篇:Kotlin学习教程(五)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%94).md) -[下一篇:Kotlin学习教程(七)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%83).md) - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! - diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\215\201).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\215\201).md" index e70017a7..7e4012f9 100644 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\215\201).md" +++ "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\215\201).md" @@ -1,40 +1,7 @@ Kotlin学习教程(十) === - -### `Kotlin`用到的关键字 - -- `var`:定义变量 -- `val`:定义常量 -- `fun`:定义方法 -- `Unit`:默认方法返回值,类似于`Java`中的`void`,可以理解成返回没什么用的值 -- `vararg`:可变参数 -- `$`:字符串模板(取值) -- 位运算符:`or`(按位或),`and`(按位与),`shl`(有符号左移),`shr`(有符号右移), -- `ushr`(无符号右移),`xor`(按位异或),`inv`(按位取反) -- `in`:在某个范围中 检查值是否在或不在(`in/!in`)范围内或集合中 -- `downTo`:递减,循环时可用,每次减1 -- `step`:步长,循环时可用,设置每次循环的增加或减少的量 -- `when`:`Kotlin`中增强版的`switch`,可以匹配值,范围,类型与参数 -- `is`:判断类型用,类似于`Java`中的`instanceof()`,`is`运算符检查表达式是否是类型的实例。 如果一个不可变的局部变量或属性是指定类型, -则不需要显式转换 -- `private`仅在同一个文件中可见 -- `protected`同一个文件中或子类可见 -- `public`所有调用的地方都可见 -- `internal`同一个模块中可见 -- `abstract`抽象类标示 -- `final`标示类不可继承,默认属性 -- `enum`标示类为枚举 -- `open`类可继承,类默认是`final`的 -- `annotation`注解类 -- `init`主构造函数不能包含任何的代码。初始化的代码可以放到以`init`关键字作为前缀的初始化块(`initializer blocks`)中 -- `field`只能用在属性的访问器内。特别注意的是,`get set`方法中只能能使用`filed`。属性访问器就是`get set`方法。 -- `:`用于类的继承,变量的定义 -- `..`围操作符(递增的) `1..5`,`2..6`千万不要`6..2` -- `::`作用域限定符 -- `inner`类可以标记为`inner {: .keyword }`以便能够访问外部类的成员。内部类会带有一个对外部类的对象的引用 -- `object`对象声明并且它总是在`object{: .keyword }`关键字后跟一个名称。对象表达式:在要创建一个继承自某个(或某些)类型的匿名类的对象会 -用到 +- [上一篇:Kotlin学习教程(九)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B9%9D).md) diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\233\233).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\233\233).md" deleted file mode 100644 index 3cc7aa0c..00000000 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\233\233).md" +++ /dev/null @@ -1,581 +0,0 @@ -Kotlin学习教程(四) -=== - - -### 数据类:使用`data class`定义 - -数据类是一种非常强大的类。在[Kotlin学习教程(一)][1]中最开始的用的简洁的示例代码就是一个数据类。这里我们再拿过来: -```java -public class Artist { - private long id; - private String name; - private String url; - private String mbid; - - public long getId() { - return id; - } - - - public void setId(long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getUrl() { - return url; - } - - public void setUrl(String url) { - this.url = url; - } - - public String getMbid() { - return mbid; - } - - public void setMbid(String mbid) { - this.mbid = mbid; - } - - @Override public String toString() { - return "Artist{" + - "id=" + id + - ", name='" + name + '\'' + - ", url='" + url + '\'' + - ", mbid='" + mbid + '\'' + - '}'; - } -} -``` -使用`Kotlin`: -```kotlin -data class Artist( - var id: Long, - var name: String, - var url: String, - var mbid: String) -``` - -通过数据类,会自动提供以下函数: -- 所有属性的`get() set()`方法 -- `equals()` -- `hashCode()` -- `copy()` -- `toString()` -- 一系列可以映射对象到变量中的函数(后面再说)。 - -如果我们使用不可修改的对象,就像我们之前讲过的,假如我们需要修改这个对象状态,必须要创建一个新的一个或者多个属性被修改的实例。 -这个任务是非常重复且不简洁的。 - -举个例子,如果要修改`Person`类中`charon`的`age`: - -```kotlin -data class Person(val name: String, - val age: Int) -``` - -```kotlin -val charon = Person("charon", 18) -val charon2 = charon.copy(age = 19) -``` -如上,我们拷贝了`charon`对象然后只修改了`age`的属性而没有修改这个对象的其它状态。 - -### 多声明 - -多声明,也可以理解为变量映射,这就是编译器自动生成的`componentN()`方法。 - -```kotlin -var personD = PersonData("PersonData", 20, "male") -var (name, age) = personD - - -Log.d("test", "name = $name, age = $age") - -//输出 -name = PersonData, age = 20 -``` - -上面的多声明,大概可以翻译成这样: - -```kotlin -var name = f1.component1() -var age = f1.component2() -``` - - -### 继承 - -在`Kotlin`中所有类都有一个共同的超类`Any`,这对于没有超类型声明的类是默认超类: -```kotlin -class Person // 从 Any 隐式继承 -``` - -`Any`不是`java.lang.Object`。它除了`equals()`、`hashCode()`和`toString()`外没有任何成员。 -`Kotlin`中所有的类默认都是不可继承的(`final`),为什么要这样设计呢?引用`Effective Java`书中的第17条:要么为继承而设计,并提供文档说明, -要么就禁止继承。所以我们只能继承那些明确声明`open`或者`abstract`的类:要声明一个显式的超类型,我们把类型放到类头的冒号之后: -```kotlin -open class Person(num: Int) -// 继承 -class SuperPerson(num: Int) : Person(num) -``` -如果该类有一个主构造函数,其基类必须用基类型的主构造函数参数就地初始化。 -如果类没有主构造函数,那么每个次构造函数必须使用`super`关键字初始化其基类型,或委托给另一个构造函数做到这一点。 -注意,在这种情况下,不同的次构造函数可以调用基类型的不同的构造函数: -```kotlin -class MyView : View { - constructor(ctx: Context) : super(ctx) - constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs) -} -``` - -### 覆盖 - -##### 方法覆盖 - - -只能重写显示标注可覆盖的方法: -```kotlin -open class Person(num: Int) { - open fun changeName(name: String) { - - } - - fun changeAge(age: Int) { - - } -} - -class SuperPerson(num: Int) : Person(num) { - override fun changeName(name: String) { - // 通过super关键字调用超类实现 - super.changeName(name) - } -} -``` -`SuperPerson.changeName()`方法前面必须加上`override`标注,不然编译器将会报错。如果像上面`Person.changeAge()`方法没有标注`open`, -则子类中不能定义相同的方法: -```kotlin -class SuperPerson(num: Int) : Person(num) { - override fun changeName(name: String) { - super.changeName(name) - } - - // 编译器报错 - fun changeAge(age: Int) { - - } - // 重载是可以的 - fun changeAge(name: String) { - - } - // 重载是可以的 - fun changeAge(age: Int, name: String) { - - } -} -``` - -标记为`override`的成员本身是开放的,也就是说,它可以在子类中覆盖。如果你想禁止再次覆盖,可以使用`final`关键字: - -```kotlin -open class SuperPerson(num: Int) : Person(num) { - final override fun changeName(name: String) { - super.changeName(name) - } -} -``` - -##### 属性覆盖 - -属性覆盖与方法覆盖类似,只能覆盖显示标明`open`的属性,并且要用`override`开头: - -```kotlin -open class Person(num: Int) { - open val name: String = "" - - open fun changeName(name: String) { - - } - - fun changeAge(age: Int) { - - } -} - -open class SuperPerson(num: Int) : Person(num) { - override val name: String - get() = super.name - - final override fun changeName(name: String) { - super.changeName(name) - } - -} -``` - -每个声明的属性可以由具有初始化器的属性或者具有`get`方法的属性覆盖,你也可以用一个`var`属性覆盖一个`val`属性,但反之则不行。 - - - -### 抽象类 - -类和其中的某些成员可以声明为`abstract`。抽象成员在本类中可以不用实现。 需要注意的是,我们并不需要用`open`标注一个抽象类或者函数——因为这不 -言而喻。 - -我们可以用一个抽象成员覆盖一个非抽象的开放成员: -```kotlin -open class Base { - open fun f() {} -} - -abstract class Derived : Base() { - override abstract fun f() -} -``` - -### 修饰符 - -`Kotlin`中修饰符是与`Java`中的有些不同。在`kotlin`中默认的修饰符是`public`,这节约了很多的时间和字符。 - -- `private` - `private`修饰符是最限制的修饰符,和`Java`中`private`一样。它表示它只能被自己所在的文件可见。所以如果我们给一个类声明为`private`, - 我们就不能在定义这个类之外的文件中使用它。 - 另一方面,如果我们在一个类里面使用了private修饰符,那访问权限就被限制在这个类里面了。甚至是继承这个类的子类也不能使用它。 - -- `protected`. - 与`Java`一样,它可以被成员自己和继承它的成员可见。 - -- `internal` - 如果是一个定义为`internal`的包成员的话,对所在的整个`module`可见。如果它是一个其它领域的成员,它就需要依赖那个领域的可见性了。 - 比如如果写了一个`private`类,那么它的`internal`修饰的函数的可见性就会限制与它所在的这个类的可见性。 - -- `public`. - 你应该可以才想到,这是最没有限制的修饰符。这是默认的修饰符,成员在任何地方被修饰为public,很明显它只限制于它的领域。 - - -### 数组 - -数组用类`Array`实现,并且还有一个`size`属性及`get`和`set`方法,由于使用`[]`重载了`get`和`set`方法,所以我们可以通过下标很方便的获取或者 -设置数组对应位置的值。 -`Kotlin`标准库提供了`arrayOf()`创建数组和`xxArrayOf`创建特定类型数组 -```kotlin -val array = arrayOf(1, 2, 3) -val countries = arrayOf("UK", "Germany", "Italy") -val numbers = intArrayOf(10, 20, 30) -val array1 = Array(10, { k -> k * k }) -val longArray = emptyArray() -val studentArray = Array(2) -studentArray[0] = Student("james") -``` - -和`Java`不一样的是`Kotlin`的数组是容器类,提供了`ByteArray`,`CharArray`,`ShortArray`,`IntArray`,`LongArray`,`BooleanArray`, -`FloatArray`和`DoubleArray`。 - -### 集合 - - -`Kotlin`的`List`类型是一个提供只读操作如`size`、`get`等的接口。和`Java`类似,它继承自`Collection`进而继承自`Iterable`。 -改变`list`的方法是由`MutableList`加入的。这一模式同样适用于`Set/MutableSet`及`Map/MutableMap`。 - -`Kotlin`没有专门的语法结构创建`list`或`set`。要用标准库的方法如`listOf()`、`mutableListOf()`、`setOf()`、`mutableSetOf()`。 -创建`map`可以用`mapOf(a to b, c to d)`。 - -```kotlin -fun main(args : Array) { - var lists = listOf("a", "b", "c") - for(list in lists) { - println(list) - } -} -``` - -```kotlin -fun main(args : Array) { - var map = TreeMap() - map["0"] = "0 haha" - map["1"] = "1 haha" - map["2"] = "2 haha" - - println(map["1"]) -} -``` - -```kotlin -val numbers: MutableList = mutableListOf(1, 2, 3) -val readOnlyView: List = numbers -println(numbers) // 输出 "[1, 2, 3]" -numbers.add(4) -println(readOnlyView) // 输出 "[1, 2, 3, 4]" -readOnlyView.clear() // -> 不能编译 - -val strings = hashSetOf("a", "b", "c", "c") -assert(strings.size == 3) -``` - - -### 可`null`类型 - - -因为在`Kotlin`中一切都是对象,一切都是可`null`的。当某个变量的值可以为`null`的时候,必须在声明处的类型后添加`?`来标识该引用可为空。 -`Kotlin`通过`?`将是否允许为空分割开来,比如`str:String`为不能空,加上`?`后的`str:String?`为允许空,通过这种方式,将本是不能确定的变 -量人为的加入了限制条件。而不符合条件的输入,则会在`IDE`上显示编译错误而无法执行。 - -```kotlin -var value1: String -value1 = null // 编译错误 Null can not be a value of a non-null type String - -var value2 : String? -value2 = null // 编译通过 -``` -在对变量进行操作时,如果变量是可能为空的,那么将不能直接调用,因为编译器不知道你的变量是否为空,所以编译器就要求你一定要对变量进行判断 -```kotlin -var str : String? = null -// 编译错误 Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String? -str.length -// 编译能通过,这表示如果str不为空的时候执行length方法 -str?.length -``` - -那么问题来了,我们知道在`java`中`String.length`返回的是`int`,上面的`str?.length`既然编译通过了,那么它返回了什么?我们可以这么写: - -`var result = str?.length` - -这么写编译器是能通过的,那么`result`的类型是什么呢?在`Kotlin`中,编译器会自动根据结果判断变量的类型,翻译成普通代码如下: - -```kotlin -if(str == null) - result = null; // 这里result为一个引用类型 -else - result = str.length; // 这里result为Int -``` -那么如果我们需要的就是一个`Int`的结果(事实上大部分情况都是如此),那又该怎么办呢?在`kotlin`中除了`?`表示可为空以外,还有一个新的符号`:`双 -感叹号`!!`,表示一定不能为空。所以上面的例子,如果要对`result`进行操作,可以这么写: -```kotlin -var str : String? = null -var result : Int = str!!.length -``` - -这样的话,就能保证`result`的数据类型,但是这样还有一个问题,那就是`str`的定义是可为空的,上面的代码中,`str`就是空,这时候下面的操作虽然 -不会报编译异常,但是运行时就会见到我们熟悉的空指针异常`NullPointerExectpion`,这显然不是我们希望见到的,也不是`kotlin`愿意见到的。 -`java`中的三元操作符大家应该都很熟悉了,`kotlin`中也有类似的,它很好的解决了刚刚说到的问题。在`kotlin`中,三元操作符是`?:`,写起来也 -比`java`要方便一些。 - -```kotlin -var str : String? = null -var result = str?.length ?: -1 -//等价于 -var result : Int = if(str != null) str.length else -1 -``` - -`if null`缩写 - -```kotlin -val data = …… -val email = data["email"] ?: throw IllegalStateException("Email is missing!") -``` - -如果`?:`左侧表达式非空,`elvis`操作符就返回其左侧表达式,否则返回右侧表达式。 -请注意,当且仅当左侧为空时,才会对右侧表达式求值。 - - -##### `!!`操作符 - -我们可以写`b!!`,这会返回一个非空的`b`值 -(例如:在我们例子中的`String`)或者如果`b`为空,就会抛出一个空指针异常: -```kotlin -val l = b!!.length -``` - -因此,如果你想要一个 NPE,你可以得到它,但是你必须显式要求它,否则它不会不期而至。 - - -#### 安全的类型转换 - -如果对象不是目标类型,那么常规类型转换可能会导致`ClassCastException`。 -另一个选择是使用安全的类型转换,如果尝试转换不成功则返回`null{: .keyword }`: - -```kotlin -val aInt: Int? = a as? Int -``` - -#### 可空类型的集合 - -如果你有一个可空类型元素的集合,并且想要过滤非空元素,你可以使用`filterNotNull`来实现。 -```kotlin -val nullableList: List = listOf(1, 2, null, 4) -val intList: List = nullableList.filterNotNull() -``` - -### 表达式 - -##### `if`表达式 - -在`Kotlin`中,`if`是一个表达式,即它会返回一个值。因此就不需要三元运算符`条件 ? 然后 : 否则`,因为普通的`if`就能胜任这个角色。 -`if`的分支可以是代码块,最后的表达式作为该块的值: -```kotlin -val max = if (a > b) { - print("Choose a") - a -} else { - print("Choose b") - b -} -``` - - -##### `when`表达式 - -`when`表达式与`Java`中的`switch/case`类似,但是要强大得多。这个表达式会去试图匹配所有可能的分支直到找到满意的一项。然后它会运行右边的表达 -式。 -与`Java`的`switch/case`不同之处是参数可以是任何类型,并且分支也可以是一个条件。 - -对于默认的选项,我们可以增加一个`else`分支,它会在前面没有任何条件匹配时再执行。条件匹配成功后执行的代码也可以是代码块: -```kotlin -when (x){ - 1 -> print("x == 1") - 2 -> print("x == 2") - else -> { - print("I'm a block") - print("x is neither 1 nor 2") - } -} -``` - -因为它是一个表达式,它也可以返回一个值。我们需要考虑什么时候作为一个表达式使用,它必须要覆盖所有分支的可能性或者实现`else`分支。否则它不会被 -编译成功: - -```kotlin -val result = when (x) { - 0, 1 -> "binary" - else -> "error" -} -``` - -如你所见,条件可以是一系列被逗号分割的值。但是它可以更多的匹配方式。比如,我们可以检测参数类型并进行判断: - -```kotlin -when(view) { - is TextView -> view.setText("I'm a TextView") - is EditText -> toast("EditText value: ${view.getText()}") - is ViewGroup -> toast("Number of children: ${view.getChildCount()} ") - else -> view.visibility = View.GONE -} -``` - -##### for循环 - -```kotlin -val items = listOf("apple", "banana", "kiwi") -for (item in items) { - println(item) -} - -for (i in array.indices) - print(array[i]) -``` - -### 使用类型检测及自动类型转换 - -`is`运算符检测一个表达式是否某类型的一个实例。 如果一个不可变的局部变量或属性已经判断出为某类型,那么检测后的分支中可以直接当作该类型使用, -无需显式转换: - -```kotlin -fun getStringLength(obj: Any): Int? { - if (obj !is String) return null - - // `obj` 在这一分支自动转换为 `String` - return obj.length -} -``` - -### 返回和跳转 - -`Kotlin`有三种结构化跳转表达式: - -- `return`:默认从最直接包围它的函数或者匿名函数返回。 -- `break`:终止最直接包围它的循环。 -- `continue`:继续下一次最直接包围它的循环。 - -在`Kotlin`中任何表达式都可以用标签`label`来标记。标签的格式为标识符后跟`@`符号,例如:`abc@`、`fooBar@`都是有效的标签。 - -要为一个表达式加标签,我们只要在其前加标签即可。 -```kotlin -loop@ for (i in 1..100) { - for (j in 1..100) { - if (……) break@loop - } -} -``` - - -### Ranges - -`Range`表达式使用一个`..`操作符。表示就是一个该范围内的数据的数组,包含头和尾 - -```kotlin -var nums = 1..100 -for(num in nums) { - println(num) - // 打印出1 2 3 ....100 -} -``` - -```kotlin -if(i >= 0 && i <= 10) - println(i) -``` -转换成 - -```kotlin -if (i in 0..10) - println(i) -``` -Ranges默认会自增长,所以如果像以下的代码: -```kotlin -for (i in 10..0) - println(i) -``` -它就不会做任何事情。但是你可以使用`downTo`函数: -```kotlin -for(i in 10 downTo 0) - println(i) -``` - -我们可以在`Ranges`中使用`step`来定义一个从`1`到一个值的不同的空隙: -```kotlin -for (i in 1..4 step 2) println(i) -for (i in 4 downTo 1 step 2) println(i) -``` - -### Until - -上面的`Range`是包含了头和尾,那如果只想包含头不包含尾呢? 就要用`until` - -```kotlin -var nums = 1 until 100 -for(num in nums) { - println(num) - // 这样打印出来是1 2 3 .....99 -} -``` - - -[上一篇:Kotlin学习教程(三)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%89).md) -[下一篇:Kotlin学习教程(五)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%94).md) - - -[1]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%80).md "Kotlin学习教程(一)" - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! - From f38bb8f2c5427b9d9e0e4852e6874ecdf64d6a34 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 8 Apr 2021 17:26:40 +0800 Subject: [PATCH 050/213] update kotlin notes --- ...&\347\261\273&\346\216\245\345\217\243.md" | 185 +++-- ...76\350\256\241\346\250\241\345\274\217.md" | 79 +- ...05\350\201\224\345\207\275\346\225\260.md" | 37 +- ...0\347\273\204&\351\233\206\345\220\210.md" | 50 +- ...7&\345\205\263\351\224\256\345\255\227.md" | 15 +- ...2\344\270\276&\345\247\224\346\211\230.md" | 134 ++-- ...47\346\211\277\351\227\256\351\242\230.md" | 12 +- ...5\345\260\204&\346\211\251\345\261\225.md" | 351 +++++++-- .../8.Kotlin_\345\215\217\347\250\213.md" | 702 +++++++++++++++++- .../9.Kotlin_androidktx.md | 160 +--- ...\346\225\231\347\250\213(\345\205\253).md" | 460 ------------ ...\346\225\231\347\250\213(\345\215\201).md" | 13 - ...72\347\241\200\347\237\245\350\257\206.md" | 8 +- ...04\344\273\266\345\260\201\350\243\205.md" | 45 ++ ...47\350\203\275\344\274\230\345\214\226.md" | 35 + .../HLS.md" | 8 + 16 files changed, 1366 insertions(+), 928 deletions(-) rename "KotlinCourse/11.Kotlin_\350\256\276\350\256\241\346\250\241\345\274\217.md" => "KotlinCourse/10.Kotlin_\350\256\276\350\256\241\346\250\241\345\274\217.md" (75%) rename "KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\271\235).md" => KotlinCourse/9.Kotlin_androidktx.md (56%) delete mode 100644 "KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\253).md" delete mode 100644 "KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\215\201).md" create mode 100644 "VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/11.\346\222\255\346\224\276\347\273\204\344\273\266\345\260\201\350\243\205.md" diff --git "a/KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" "b/KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" index b4432c22..995523d5 100644 --- "a/KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" +++ "b/KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" @@ -1,4 +1,4 @@ -1.Kotlin_简介 +1.Kotlin_简介&变量&类&接口 === ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_kotlin.jpeg?raw=true) @@ -13,7 +13,7 @@ [Kotlin官网](https://kotlinlang.org/) `JetBrains`这家公司非常牛逼,开发了很多著名的软件,他们在使用`Java`的过程中发现`java`比较笨重不方便,所以就开发了`kotlin`,`kotlin`是 -一种全栈的开发语言,可以用它进行开发`web`、`web`后端、`Android`等。 +一种全栈的开发语言,可以用它进行开发`web`、`web`后端、`Android`等。 但是JetBrains团队设计Kotlin所要面临的第一个问题就是必须兼容他们所拥有的数百万行Java代码库,这也代表了Kotlin基于整个Java社区所承载的使命之一,即需要与现有的Java代码完全兼容。这个背景也决定了Kotlin的核心目标--为Java程序员提供一门更好的变成语言。 很多开发者都说`Google`学什么不好,非要学苹果,出个`android`的`swift`版本,一定会搞不起来没人用,所以不用浪费时间去学习。在这里想引用马云 的一句话: @@ -113,8 +113,7 @@ data class Person( ## 创建`Kotlin`项目 -`Google`宣布在`Android Studio 3.0`版本会全面支持`Kotlin`,目前早就有预览版了 -[Android Studio Preview](https://developer.android.com/studio/preview/index.html)(个人感觉很好用,比2.3.3版本强多了)。 +`Google`宣布在`Android Studio 3.0`版本会全面支持`Kotlin`, 直接通过`New Project`创建就可以,与创建普通`Java`项目唯一不同的是要勾选`Include Kotlin support`的选项。 @@ -143,7 +142,7 @@ class MainActivity : AppCompatActivity() { } ``` -我们就从`MainActivity`的代码开始介绍一些基本的语法。 + ## 变量 @@ -161,7 +160,7 @@ book.printName() // Diving into Kotlin 再提示一下:`kotlin`中每行代码结束不需要分号了,不要和`java`是的每行都带分号 -字面上可以写明具体的类型。这个不是必须的,但是一个通用的`Kotlin`实践时省略变量的类型我们可以让编译器自己去推断出具体的类型: +字面上可以写明具体的类型。这个不是必须的,但是一个通用的`Kotlin`实践时省略变量的类型我们可以让编译器自己去推断出具体的类型,**Kotlin拥有比Java更加强大的类型推导功能,这避免了静态类型语言在编码时需要书写大量类型的弊端**: ```kotlin var age = 18 // int val name = "charon" // string @@ -170,8 +169,7 @@ var weight = 70.5 // double ``` 在`Kotlin`中,一切都是对象。没有像`Java`中那样的原始基本类型。 -当然,像`Integer`,`Float`或者`Boolean`等类型仍然存在,但是它们全部都会作为对象存在的。基本类型的名字和它们工作方式都是与`Java`非常相似 -的,但是有一些不同之处你可能需要考虑到: +当然,像`Integer`,`Float`或者`Boolean`等类型仍然存在,但是它们全部都会作为对象存在的。基本类型的名字和它们工作方式都是与`Java`非常相似的,但是有一些不同之处你可能需要考虑到: - 数字类型中不会自动转型。举个例子,你不能给`Double`变量分配一个`Int`。必须要做一个明确的类型转换,可以使用众多的函数之一: ```kotlin @@ -216,10 +214,10 @@ var weight = 70.5 // double - IO操作,如写数据到磁盘 - UI操作,如修改了一个按钮的可操作状态 -来看一个实际的例子:先用va来声明一个变量a,然后在count函数内部对其进行自增操作: +来看一个实际的例子:先用var来声明一个变量a,然后在count函数内部对其进行自增操作: ```kotlin -val a = 1 +var a = 1 fun count(x: Int) { a = a + 1 println(x + a) @@ -252,6 +250,8 @@ class MainActivity : AppCompatActivity() { ## 后端变量`Backing Fields`. +`Kotlin`会默认创建`set get`方法,我们也可以自定义`get set`方法: + 在`kotlin`的`getter`和`setter`是不允许本身的局部变量的,因为属性的调用也是对`get`的调用,因此会产生递归,造成内存溢出。 例如: @@ -259,21 +259,42 @@ class MainActivity : AppCompatActivity() { ```kotlin var count = 1 var size: Int = 2 -set(value) { - Log.e("text", "count : ${count++}") - size = if (value > 10) 15 else 0 -} + set(value) { + Log.e("text", "count : ${count++}") + size = if (value > 10) 15 else 0 + } ``` 这个例子中就会内存溢出。 `kotlin`为此提供了一种我们要说的后端变量,也就是`field`。编译器会检查函数体,如果使用到了它,就会生成一个后端变量,否则就不会生成。 -我们在使用的时候,用`field`代替属性本身进行操作。 +我们在使用的时候,用`field`代替属性本身进行操作。按照惯例`set`参数的名称是`value`,但是如果你喜欢你可以选择一个不同的名称。 + +```kotlin +class A { + var count = 1 + var size: Int = 2 + set(value) { + field = if (value > 10) 15 else 0 + } + get() { + return if (field == 15) 1 else 0 + } +} +fun main() { + val a = A() + a.size = 11 + println("${a.size}") +} +// +1 +``` + + ## 延迟初始化 我们说过,在类内声明的属性必须初始化,如果设置非`null`的属性,应该将此属性在构造器内进行初始化。 -假如想在类内声明一个`null`属性,在需要时再进行初始化(最典型的就是懒汉式单例模式),与`Kotlin`的规则是相背的,此时我们可以声明一个属性并 -延迟其初始化,此属性用`lateinit`修饰符修饰。 +假如想在类内声明一个`null`属性,在需要时再进行初始化(最典型的就是懒汉式单例模式),与`Kotlin`的规则是相背的,此时我们可以声明一个属性并延迟其初始化,此属性用`lateinit`修饰符修饰。 ```kotlin class MainActivity : AppCompatActivity() { @@ -628,10 +649,6 @@ val charon2 = charon.copy(age = 19) - data class之前不能用abstract、open、sealed或者inner进行修饰 - 在Kotlin 1.1版本前数据类只允许实现接口,之后的版本既可以实现接口也可以继承类 - - - - ## 多声明 多声明,也可以理解为变量映射,这就是编译器自动生成的`componentN()`方法。 @@ -718,6 +735,32 @@ public final class Bird { +### Any + +我们都知道,Java并不能在真正意义上被称为一门“ 纯面向对象”语言,因为它的原始类型(如int)的值与函数等并不能被视作对象。 + +但是Kotlin不同,在Kotlin的类型系统中,并不区分原始类型(基本数据类型)和包装类型,我们使用的始终是同一个类型。虽然从严格意义上,我们不能说Kotlin是一门纯面向对象的语言,但它显然比Java有更纯的设计。 + + + +#### Any:非空类型的跟类型 + +与Object作为Java类层级结构的顶层类似,Any类型是Kotlin中所有非空类型(如String、Int)的超类,如: + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/kotlin_any.png?raw=true) + +与Java不同的是,Kotlin不区分“原始类型”(primitive type)和其他的类型,他们都是同一类型层级结构的一部分。 如果定义了一个没有指定父类型的类型,则该类型将是Any的直接子类型。如: + +```kotlin +class Animal(val weight: Double) +``` + +#### Any?:所有类型的根类型 + +如果说Any是所有非空类型的根类型,那么Any?才是所有类型(可空和非空类型)的根类型。这也就是说?Any?是?Any的父类型。 + + + ## 覆盖 ##### 方法覆盖 @@ -812,8 +855,7 @@ open class SuperPerson(num: Int) : Person(num) { ## 抽象类 -类和其中的某些成员可以声明为`abstract`。抽象成员在本类中可以不用实现。 需要注意的是,我们并不需要用`open`标注一个抽象类或者函数——因为这不 -言而喻。 +类和其中的某些成员可以声明为`abstract`。抽象成员在本类中可以不用实现。 需要注意的是,我们并不需要用`open`标注一个抽象类或者函数——因为这不言而喻。 我们可以用一个抽象成员覆盖一个非抽象的开放成员: @@ -859,7 +901,7 @@ interface FlyingAnimal { interface Flyer { val height = 1000 // error Property initializers are not allowed in interfaces val speed: Int - // 可以支持默认实现方法,反编译可以看到是通过静态内部类来提供fly方法的默认实现的 + // 可以支持默认实现方法,反编译可以看到是通过静态内部类来提供fly方法的默认实现的,Java8也开始支持了接口方法的默认实现 fun fly() { println("I can fly") } @@ -908,8 +950,6 @@ fun add(x: Int,y: Int) : Int = x + y // 省略了{} Kotlin支持这种单行表达式与等号的语法来定义函数,叫做表达式函数体,作为区分,普通的函数声明则可以叫做代码块函数体。如你所见,在使用表达式函数体的情况下我们可以不声明返回值类型,这进一步简化了语法。 - - 我们可以给参数指定一个默认值使得它们变得可选,这是非常有帮助的。这里有一个例子,在`Activity`中创建了一个函数用来`Toast`一段信息: ```kotlin @@ -924,27 +964,7 @@ toast("Hello") toast("Hello", Toast.LENGTH_LONG) ``` -### 自定义`get set`方法: - -`Kotlin`会默认创建`set get`方法,我们也可以自定义`get set`方法: -`kotlin`预留了一个在`set`和`get`中访问的变量`field`关键字: - -```kotlin -class Person constructor() { - var name: String = "" - get() = field - set(value) { - field = "$value" - } - var age: Int = 0 - get() = field - set(value) { - field = value - } -} -``` -按照惯例`set`参数的名称是`value`,但是如果你喜欢你可以选择一个不同的名称。 ### 可变长参数函数:使用`vararg`关键字 @@ -961,7 +981,33 @@ fun main(args: Array) { } ``` -### 命名风格 + + +### `Unit`:让函数调用皆为表达式 + +如果函数返回`Unit`类型,该返回类型应该省略: + +```kotlin +fun foo() { // 省略了 ": Unit" + +} +``` + +之所以不能说Java中的函数调用皆是表达式,是因为存在特例void。众所周知,在Java中如果声明的函数没有返回值,那么它就需要用void来修饰,如: + +```java +void foo() { + System.out.println("return nothing") +} +``` + +所以foo()就不具有值和类型信息,它就不能算作一个表达式。在Kotlin中,函数在所有的情况下都具有返回类型,所以他们引入了Unit来替代Java中的void关键字。 + +Unit与Int一样,都是一种类型,然而它不代表任何信息,用面向对象的术语来描述就是一个单例,它的实例只有一个,可写为()。 + + + +## 命名风格 如果拿不准的时候,默认使用`Java`的编码规范,比如: @@ -972,6 +1018,29 @@ fun main(args: Array) { - 公有函数应撰写函数文档,这样这些文档才会出现在`Kotlin Doc`中 + +### 类布局 + +通常,一个类的内容按以下顺序排列: + +- 属性声明与初始化块 +- 次构造函数 +- 方法声明 +- 伴生对象 + +不要按字母顺序或者可见性对方法声明排序,也不要将常规方法与扩展方法分开。而是要把相关的东西放在一起,这样从上到下阅读类的人就能够跟进所发生事情的逻辑。选择一个顺序(高级别优先,或者相反)并坚持下去。 + +将嵌套类放在紧挨使用这些类的代码之后。如果打算在外部使用嵌套类,而且类中并没有引用这些类,那么把它们放到末尾,在伴生对象之后。 + +### 接口实现布局 + +在实现一个接口时,实现成员的顺序应该与该接口的成员顺序相同(如果需要, 还要插入用于实现的额外的私有方法) + +### 重载布局 + +在类中总是将重载放在一起。 + + ### 冒号 类型和超类型之间的冒号前要有一个空格,而实例和类型之间的冒号前不要有空格: @@ -1016,28 +1085,6 @@ class Person( } ``` -### `Unit`:让函数调用皆为表达式 - -如果函数返回`Unit`类型,该返回类型应该省略: - -```kotlin -fun foo() { // 省略了 ": Unit" - -} -``` - -之所以不能说Java中的函数调用皆是表达式,是因为存在特例void。众所周知,在Java中如果声明的函数没有返回值,那么它就需要用void来修饰,如: - -```java -void foo() { - System.out.println("return nothing") -} -``` - -所以foo()就不具有值和类型信息,它就不能算作一个表达式。在Kotlin中,函数在所有的情况下都具有返回类型,所以他们引入了Unit来替代Java中的void关键字。 - -Unit与Int一样,都是一种类型,然而它不代表任何信息,用面向对象的术语来描述就是一个单例,它的实例只有一个,可写为()。 - [下一篇:Kotlin学习教程(二)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%8C).md) diff --git "a/KotlinCourse/11.Kotlin_\350\256\276\350\256\241\346\250\241\345\274\217.md" "b/KotlinCourse/10.Kotlin_\350\256\276\350\256\241\346\250\241\345\274\217.md" similarity index 75% rename from "KotlinCourse/11.Kotlin_\350\256\276\350\256\241\346\250\241\345\274\217.md" rename to "KotlinCourse/10.Kotlin_\350\256\276\350\256\241\346\250\241\345\274\217.md" index 5a3a5989..fc6a644e 100644 --- "a/KotlinCourse/11.Kotlin_\350\256\276\350\256\241\346\250\241\345\274\217.md" +++ "b/KotlinCourse/10.Kotlin_\350\256\276\350\256\241\346\250\241\345\274\217.md" @@ -1,11 +1,11 @@ -11.Kotlin_设计模式 +10.Kotlin_设计模式 === - ## 工厂模式 -简单工厂的模式,它的核心作用是通过一个工厂类隐藏对象实例的创建逻辑,而不需要暴露给客户端。典型的使用场景就是当拥有一个父类与多个子类的时候,我们可以通过这种模式来创建子类对象。 +简单工厂的模式,它的核心作用是通过一个工厂类隐藏对象实例的创建逻辑,而不需要暴露给客户端。典型的使用场景就是当拥有一个父类 +与多个子类的时候,我们可以通过这种模式来创建子类对象。 假设现在有一个电脑加工厂,同时生产个人电脑和服务器主机。我们用熟悉的工厂模式设计描述其业务逻辑: @@ -34,7 +34,9 @@ fun main() { } ``` -以上代码通过调用ComputerFactory类的produce方法来创建不同的Computer子类对象,这样我们就把创建实例的逻辑和客户端之间实现解耦。这是用Kotlin模仿Java中很标准的工厂模式设计,它改善了程序的可维护性,但创建对象的表达上却显得不够简洁。当我们在不同的地方创建Computer的子类对象时,我们都需要先创建一个ComputerFactory类对象。 +以上代码通过调用ComputerFactory类的produce方法来创建不同的Computer子类对象,这样我们就把创建实例的逻辑和客户端之间实现解耦。 +这是用Kotlin模仿Java中很标准的工厂模式设计,它改善了程序的可维护性,但创建对象的表达上却显得不够简洁。 +当我们在不同的地方创建Computer的子类对象时,我们都需要先创建一个ComputerFactory类对象。 ### 用单例代替工厂类 @@ -56,7 +58,8 @@ fun main() { } ``` -由于我们通过传入Computer类型来创建不同的对象,所以这里的produce又显得多余。我们可以用运算符重载来通过operator操作符重载invoke方法来代替produce,从而进一步简化表达: +由于我们通过传入Computer类型来创建不同的对象,所以这里的produce又显得多余。我们可以用运算符重载来通过operator操作符 +重载invoke方法来代替produce,从而进一步简化表达: ```kotlin object ComputerFactory { @@ -101,7 +104,8 @@ fun main() { } ``` -我们可以直接通过Computer来调用其伴生对象中的方法。当然,如果你觉得还是Factory这个名字好,那么也没有问题,我们可以用Factory来命名Computer的伴生对象,如下: +我们可以直接通过Computer来调用其伴生对象中的方法。当然,如果你觉得还是Factory这个名字好,那么也没有问题,我们可以用 +Factory来命名Computer的伴生对象,如下: ```kotlin interface Computer { @@ -126,7 +130,9 @@ fun main() { ### 扩展伴生对象方法 -依靠伴生对象的特性,我们已经很好地实现了经典的工厂模式。同时,这种方式还有一种优势,它比原有Java中的设计更加强大。假设实际业务中我们是Computer接口的使用者,比如它是工程引入的第三方类库,所有的类的实现细节都得到了很好的隐藏。那么,如果我们希望进一步改造其中的逻辑,Kotlin中伴生对象的方式同样可以依靠其扩展函数的特性,很好的实现这一需求: +依靠伴生对象的特性,我们已经很好地实现了经典的工厂模式。同时,这种方式还有一种优势,它比原有Java中的设计更加强大。 +假设实际业务中我们是Computer接口的使用者,比如它是工程引入的第三方类库,所有的类的实现细节都得到了很好的隐藏。 +那么,如果我们希望进一步改造其中的逻辑,Kotlin中伴生对象的方式同样可以依靠其扩展函数的特性,很好的实现这一需求: 比如我们希望给Computer增加一种功能,通过CPU型号来判断电脑类型,那么可以如下实现: @@ -146,7 +152,10 @@ fun main() { ### 内联函数简化抽象工厂 -Kotlin中的内联函数有一个很大的作用,就是可以具体化参数类型。利用这一特性,可以改进一种更复杂的工厂模式,称为抽象工厂。 上面的例子中已经用工厂模式很好的处理了一个产品登记结构的问题。但是如果现在引入了品牌商的概念,我们有好几个不同的电脑品牌,比如Dell、Asus、Acer,那么就有必要再增加一个工厂类。然而,我们并不希望对每个模型都建立一个工厂,这会让代码变得难以维护,所以这时候我们就需要引入抽象工厂模式。 +Kotlin中的内联函数有一个很大的作用,就是可以具体化参数类型。利用这一特性,可以改进一种更复杂的工厂模式,称为抽象工厂。 +上面的例子中已经用工厂模式很好的处理了一个产品登记结构的问题。但是如果现在引入了品牌商的概念,我们有好几个不同的电脑品牌, +比如Dell、Asus、Acer,那么就有必要再增加一个工厂类。然而,我们并不希望对每个模型都建立一个工厂,这会让代码变得难以维护, +所以这时候我们就需要引入抽象工厂模式。 @@ -185,9 +194,12 @@ fun main(args: Array) { } ``` -可以看出,每个电脑品牌拥有一个代表电脑产品的类,它们都实现了Computer接口。此外每个品牌也还有一个用于生产电脑的AbstractFactory子类,可通过AbstractFactory类的伴生对象中的invoke方法,来构造具体品牌的工厂类对象。 +可以看出,每个电脑品牌拥有一个代表电脑产品的类,它们都实现了Computer接口。此外每个品牌也还有一个用于生产电脑的 +AbstractFactory子类,可通过AbstractFactory类的伴生对象中的invoke方法,来构造具体品牌的工厂类对象。 -由于Kotlin语法的简洁,以上例子的抽象工厂类的设计也比较直观。然而,当你每次创建具体的工厂类时,都需要传入一个具体的工厂类对象作为参数进行构造,这个在语法上显然不够优雅。下面我们就来看看,如何用Kotlin中的内联函数来改善这一情况。我们所需要做的,就是去重新实现AbstractFactory类中的invoke方法。 +由于Kotlin语法的简洁,以上例子的抽象工厂类的设计也比较直观。然而,当你每次创建具体的工厂类时,都需要传入一个具体的 +工厂类对象作为参数进行构造,这个在语法上显然不够优雅。下面我们就来看看,如何用Kotlin中的内联函数来改善这一情况。 +我们所需要做的,就是去重新实现AbstractFactory类中的invoke方法。 ```kotlin abstract class AbstractFactory { @@ -205,7 +217,8 @@ abstract class AbstractFactory { } ``` -这下我们的invoke方法定义的前缀变长了很多,但是不要害怕,如果你已经掌握了内联函数的具体应用,应该会很容易理解它。我们来分析下这段代码: +这下我们的invoke方法定义的前缀变长了很多,但是不要害怕,如果你已经掌握了内联函数的具体应用,应该会很容易理解它。 +我们来分析下这段代码: - 通过将invoke方法用inline定义为内联函数,我们就可以引入reified关键字,使用具体化参数类型的语法特性。 - 要具体化的参数类型为Computer,在invoke方法中我们通过判断它的具体类型,来返回对应的工厂类对象。 @@ -228,9 +241,12 @@ fun main(args: Array) { 构造者模式与单例模式一样,它主要做的事情就是将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。 -工厂模式和构造函数都存在相同的问题,就是不能很好地扩展到大量的可选参数。假设我们现在有个机器人类,它含有多个属性:代号、名字、电池、重量、高度、速度、音量等。很多产品都不具有其中的某些属性,比如不能走、不能发声,甚至有的机器人也不需要电池。 +工厂模式和构造函数都存在相同的问题,就是不能很好地扩展到大量的可选参数。假设我们现在有个机器人类,它含有多个属性: +代号、名字、电池、重量、高度、速度、音量等。很多产品都不具有其中的某些属性,比如不能走、不能发声,甚至有的机器人也不需要电池。 -一种糟糕的做法就是设计一个一开头你所看到Robot类,把所有的属性都作为构造函数的参数。或者,你也可能采用过重叠构造器模式,即先提供一个只有必要参数的构造函数,然后再提供其他更多的构造函数,分别具有不同情况的可选属性。虽然这种模式在调用的时候改进不少,但同样存在明显的确定,因为随着构造函数的参数数量增加,很快我们就会失去控制,代码变得难以维护。 +一种糟糕的做法就是设计一个一开头你所看到Robot类,把所有的属性都作为构造函数的参数。或者,你也可能采用过重叠构造器模式, +即先提供一个只有必要参数的构造函数,然后再提供其他更多的构造函数,分别具有不同情况的可选属性。虽然这种模式在调用的时候改进不少, +但同样存在明显的缺点,因为随着构造函数的参数数量增加,很快我们就会失去控制,代码变得难以维护。 构建者模式可以避免以上问题,我们用Kotlin来实现Java中的构建者模式: @@ -281,7 +297,8 @@ var robot = Robot.Builder("007") - 通过在Builder类中定义set方法来对可选的属性进行设置 - 最终调用Builder类中的build方法来返回一个Robot对象 -这种链式调用的设计看起来确实优雅了很多,同时对于可选参数的设置也显得比较语义化。此外,构建者模式另外一个好处就是解决了多个可选参数的问题,当我们创建对象实例时,只需要用set方法对需要的参数进行赋值即可。 +这种链式调用的设计看起来确实优雅了很多,同时对于可选参数的设置也显得比较语义化。此外,构建者模式另外一个好处就是解决了 +多个可选参数的问题,当我们创建对象实例时,只需要用set方法对需要的参数进行赋值即可。 然而,构建者模式也存在一些不足: @@ -289,7 +306,8 @@ var robot = Robot.Builder("007") - 你可能会在使用Builder的时候忘记在最后调用build方法 - 由于在创建对象的时候,必须先创建它的构造器,因此额外增加了多余的开销,在某些十分注重性能的情况下,可能就存在一定的问题。 -事实上,当用Kotlin设计程序时,我们可以在绝大多数情况下避免使用构建者模式。《Effective Java》在介绍构建者模式时,是这样子描述它的:本质上builder模式模拟了具名的可选参数。幸运的是,Kotlin也是这样一门拥有具名可选参数的编程语言。 +事实上,当用Kotlin设计程序时,我们可以在绝大多数情况下避免使用构建者模式。《Effective Java》在介绍构建者模式时, +是这样子描述它的:本质上builder模式模拟了具名的可选参数。幸运的是,Kotlin也是这样一门拥有具名可选参数的编程语言。 #### 具名的可选参数 @@ -330,7 +348,8 @@ private fun main() { #### require方法对参数进行约束 -我们再来看看构建者模式的另外一个作用,就是可以在build方法中对参数添加约束条件。举个例子,假设一个机器人的重量必须根据电池的型号决定,那么在未传入电池型号之前,你便不能对weight属性进行赋值,否则就会抛出异常。现在重新修改一下上面build方法的实现: +我们再来看看构建者模式的另外一个作用,就是可以在build方法中对参数添加约束条件。举个例子,假设一个机器人的重量必须根据 +电池的型号决定,那么在未传入电池型号之前,你便不能对weight属性进行赋值,否则就会抛出异常。现在重新修改一下上面build方法的实现: ```kotlin fun build(): Robot { @@ -344,7 +363,8 @@ fun build(): Robot { 这种在build方法中对参数进行约束的手段,可以让业务变得更加安全。那么,通过具名的可选参数来构造类的方案该如何实现呢? -显然,我们同样可以在Robot类的init方法中增加以上的校检代码。然而在Kotlin中,我们在类或函数中还可以使用require关键字进行参数限制,本质上它是一个内联的方法,有点类似于Java的assert。 +显然,我们同样可以在Robot类的init方法中增加以上的校检代码。然而在Kotlin中,我们在类或函数中还可以使用require关键字 +进行参数限制,本质上它是一个内联的方法,有点类似于Java的assert。 ```kotlin class Robot( @@ -361,22 +381,22 @@ class Robot( } ``` -可见,Kotlin的require方法可以让我们的参数约束代码在语义上变得更加友好。总的来说,在Kotlin中我们应该尽量避免使用构建者模式,因为Kotlin支持具名的可选参数,这让我们可以在构造一个具有多个可选参数类的场景中,设计出更加简洁并利于维护的代码。 - - - +可见,Kotlin的require方法可以让我们的参数约束代码在语义上变得更加友好。总的来说,在Kotlin中我们应该尽量避免使用构建者模式, +因为Kotlin支持具名的可选参数,这让我们可以在构造一个具有多个可选参数类的场景中,设计出更加简洁并利于维护的代码。 ## 观察者模式 -观察者模式定义了一个一对多的依赖关系,让一个或多个观察者对象监听一个主题对象。这样一来,当被观察者状态发生改变时,需要通知相应的观察者,使这些观察者对象能够自动更新。 +观察者模式定义了一个一对多的依赖关系,让一个或多个观察者对象监听一个主题对象。这样一来,当被观察者状态发生改变时, +需要通知相应的观察者,使这些观察者对象能够自动更新。 简单来说,观察者模式无非做两件事情: - 订阅者(observer)添加或删除对发布者(publisher)的状态监听。 - 发布者状态改变时,将事件通知给监听它的所有观察者,然后观察者执行响应逻辑。 -Java自身的标准库提供了java.util.Observable类和java.util.Observer接口,来帮助实现观察者模式,接下来我们就采用它们来实现一个动态更新股价的例子。 +Java自身的标准库提供了java.util.Observable类和java.util.Observer接口,来帮助实现观察者模式,接下来我们就采用 +它们来实现一个动态更新股价的例子。 ```kotlin class StockUpdate: Observable() { @@ -400,9 +420,8 @@ fun main(args: Array) { } ``` -上面是通过Kotlin使用Java标准库中的类和方法来实现了观察者模式。事实上,Kotlin的标准库额外引入了可被观察的委托属性,也可以利用它来实现同样的场景。 - -我们可以先用这一委托属性来改造以上的程序: +上面是通过Kotlin使用Java标准库中的类和方法来实现了观察者模式。事实上,Kotlin的标准库额外引入了可被观察的委托属性, +也可以利用它来实现同样的场景。我们可以先用这一委托属性来改造以上的程序: ```kotlin import kotlin.properties.Delegates @@ -441,9 +460,13 @@ The latest stock price has risen to 100 The latest stock price has fell to 98 ``` -如果你仔细思考,会发现实现java.util.Observer接口的类只能覆写update方法来编写响应逻辑,也就是说如果存在多种不同的逻辑响应,我们也必须通过在该方法中进行区分实现,显然这会让订阅者的代码显得臃肿。换个角度,如果我们把发布者的事件推送看成一个第三方服务,那么它提供的API接口只有一个,API调用者必须承担更多的职责。 +如果你仔细思考,会发现实现java.util.Observer接口的类只能覆写update方法来编写响应逻辑,也就是说如果存在多种不同的逻辑响应, +我们也必须通过在该方法中进行区分实现,显然这会让订阅者的代码显得冗余。换个角度,如果我们把发布者的事件推送看成一个第三方服务, +那么它提供的API接口只有一个,API调用者必须承担更多的职责。 -显然,使用Delegates.observable()的方案更加灵活。它提供了三个参数,依次代表委托属性的元数据KProperty对象、旧值以及新值。通过额外定义一个StockUpdateListener接口,我们可以把上涨和下跌的不同响应逻辑封装成接口方法,从而在StockDisplay中实现该接口的onRise和onFall方法,实现了解耦。 +显然,使用Delegates.observable()的方案更加灵活。它提供了三个参数,依次代表委托属性的元数据KProperty对象、旧值以及新值。 +通过额外定义一个StockUpdateListener接口,我们可以把上涨和下跌的不同响应逻辑封装成接口方法,从而在StockDisplay中 +实现该接口的onRise和onFall方法,实现了解耦。 diff --git "a/KotlinCourse/2.Kotlin_\351\253\230\351\230\266\345\207\275\346\225\260&Lambda&\345\206\205\350\201\224\345\207\275\346\225\260.md" "b/KotlinCourse/2.Kotlin_\351\253\230\351\230\266\345\207\275\346\225\260&Lambda&\345\206\205\350\201\224\345\207\275\346\225\260.md" index 23458c79..77f1b45e 100644 --- "a/KotlinCourse/2.Kotlin_\351\253\230\351\230\266\345\207\275\346\225\260&Lambda&\345\206\205\350\201\224\345\207\275\346\225\260.md" +++ "b/KotlinCourse/2.Kotlin_\351\253\230\351\230\266\345\207\275\346\225\260&Lambda&\345\206\205\350\201\224\345\207\275\346\225\260.md" @@ -1,23 +1,6 @@ -Kotlin之Lambda&内联函数(七) +2.Kotlin_高阶函数&Lambda&内联函数 === - - -函数式语言一个典型的特征就在于函数是头等公民-我们不仅可以像类一样在顶层直接定义一个函数,也可以在一个函数内部定义一个函数,例如: - -```kotlin -fun foo(x: Int) { - fun double(y: Int): Int { - return y * 2 - } - println(double(x)) -} - -执行foo(1)结果为2 -``` - - - ## 高阶函数 Kotlin天然支持了部分函数式特性。函数式语言一个典型的特征就在于函数是头等公民——我们不仅可以像类一样在底层直接定义一个函数,也可以在一个函数内部定义一个局部函数。 @@ -33,7 +16,7 @@ fun foo(x: Int) { ### 抽象和高阶函数 -我们会善于对熟悉 或重复的事物进行抽象,比如2岁左右的小孩就会开始认识数字1、2、3....之后,我们总结除了一些公共的行为,如对数字做加减、求立方,这被称为过程,它接收的数字是一种数据,然后也可能产生另一种数据。 +我们会善于对熟悉或重复的事物进行抽象,比如2岁左右的小孩就会开始认识数字1、2、3....之后,我们总结除了一些公共的行为,如对数字做加减、求立方,这被称为过程,它接收的数字是一种数据,然后也可能产生另一种数据。 过程也是一种抽象,几乎我们所熟悉的所有高级语言都包含了定义过程的能力,也就是函数。 @@ -131,6 +114,8 @@ class CountryTest { (errCode: Int, errMsg: String) -> Unit // ?表示可选,可在某种情况下为空 ((errCode: Int, errMsg: String?) -> Unit)? + // 表示传入一个类型为Int的参数,然后返回另一个类型为(Int) -> Unit的函数 + (Int) -> ((Int) -> Unit) ``` 在学习了Kotlin函数类型知识之后,Shaw重新定义了filterCountries方法的参数声明: @@ -190,7 +175,19 @@ class Book(val name: String) { Book::name ``` -以上创建的Book::name的类型为(Book) -> String。 +以上创建的Book::name的类型为(Book) -> String。当我们再对Book类对象的集合应用一些函数式API的时候,这会显得格外有用,比如: + +```kotlin +fun main(args: Array) { + val bookNames = listOf ( + Book("Thinking in java") + Book("Dive into Kotlin") + ).map(Book::name) + println(bookNames) +} +``` + + diff --git "a/KotlinCourse/3.Kotlin_\346\225\260\345\255\227&\345\255\227\347\254\246\344\270\262&\346\225\260\347\273\204&\351\233\206\345\220\210.md" "b/KotlinCourse/3.Kotlin_\346\225\260\345\255\227&\345\255\227\347\254\246\344\270\262&\346\225\260\347\273\204&\351\233\206\345\220\210.md" index 44f17930..3cab6d55 100644 --- "a/KotlinCourse/3.Kotlin_\346\225\260\345\255\227&\345\255\227\347\254\246\344\270\262&\346\225\260\347\273\204&\351\233\206\345\220\210.md" +++ "b/KotlinCourse/3.Kotlin_\346\225\260\345\255\227&\345\255\227\347\254\246\344\270\262&\346\225\260\347\273\204&\351\233\206\345\220\210.md" @@ -1,4 +1,4 @@ -Kotlin学习教程(三) +3.Kotlin_数字&字符串&数组&集合 === 前面介绍了基本语法和编码规范后,接下来学习下基本类型。 @@ -314,15 +314,26 @@ val list = listOf(1, 2, 3, 4, 5) list.filter {it > 2}.map {it * 2} ``` -上面的写法很简洁,在处理集合时,类似于上面的操作能够帮助我们解决大部分的问题。但是list中的元素非常多的时候(比如超过10万),上面的操作在处理集合的时候就会显得比较低效。因为filter方法和map方法都会返回一个新的集合,也就是说上面的操作会产生两个临时集合,因为list会先调用filter方法,然后产生的集合会再次调用map方法。如果list中的元素非常多,这将会是一笔不小的开销。为了解决这一问题,序列(Sequence)就出现了。 +上面的写法很简洁,在处理集合时,类似于上面的操作能够帮助我们解决大部分的问题。 +但是list中的元素非常多的时候(比如超过10万),上面的操作在处理集合的时候就会显得比较低效。 +因为filter方法和map方法都会返回一个新的集合,也就是说上面的操作会产生两个临时集合, +因为list会先调用filter方法,然后产生的集合会再次调用map方法。如果list中的元素非常多, +这将会是一笔不小的开销。为了解决这一问题,序列(Sequence)就出现了。 ```kotlin list.asSequence().filter {it > 2}.map {it * 2}.toList() ``` -首先通过asSequence方法将一个列表转换为序列,然后在这个序列上进行相应的操作,最后通过toList方法将序列转为列表。将list转换为序列,在很大程度上就提高了上面操作集合的效率。因为在使用序列的时候filter方法和map方法的操作都没有创建额外的集合,这样当集合中的元素数量巨大的时候,就减少了大部分开销。在Kotlin中,序列中元素的求值是惰性的,这就意味着在利用序列进行链式求值的时候,不需要像操作普通集合那样,每进行一次求值操作,就产生一个新的集合保存中间数据。那么什么惰性又是什么意思呢? +首先通过asSequence方法将一个列表转换为序列,然后在这个序列上进行相应的操作,最后通过 +toList方法将序列转为列表。将list转换为序列,在很大程度上就提高了上面操作集合的效率。 +因为在使用序列的时候filter方法和map方法的操作都没有创建额外的集合,这样当集合中的元素数量巨大的时候, +就减少了大部分开销。在Kotlin中,序列中元素的求值是惰性的,这就意味着在利用序列进行链式求值的时候, +不需要像操作普通集合那样,每进行一次求值操作,就产生一个新的集合保存中间数据。那么什么惰性又是什么意思呢? -在编程语言理论中,惰性求值(Lazy Evaluation)表示一种在需要时才进行求值的计算方式。在使用惰性求值的时候,表达式不在它被绑定到变量之后就立即求值,而是在该值被取用时才去求值。通过这种方式,不仅能得到性能上的提升,还有一个重要的好处就是它可以构造出一个无限的数据类型。 +#### 惰性求值 +在编程语言理论中,惰性求值(Lazy Evaluation)表示一种在需要时才进行求值的计算方式。 +在使用惰性求值的时候,表达式不在它被绑定到变量之后就立即求值,而是在该值被取用时才去求值。 +通过这种方式,不仅能得到性能上的提升,还有一个重要的好处就是它可以构造出一个无限的数据类型。 @@ -351,11 +362,17 @@ list.asSequence().filter {it > 2}.map {it * 2}.toList() } ``` - 上面操作中的println方法根本没有被执行,这说明filter和map方法的执行被延迟了,这就是惰性求值的体现。惰性求值也被称为延迟求值,通过前面的定义我们知道,惰性求值仅仅在该值被需要的时候才会真正去求值。那么这个”被需要“的状态怎么去触发呢?这就需要另外一个操作了-末端操作。 + 上面操作中的println方法根本没有被执行,这说明filter和map方法的执行被延迟了,这就是惰性求值的体现。 + 惰性求值也被称为延迟求值,通过前面的定义我们知道,惰性求值仅仅在该值被需要的时候才会真正去求值。 + 那么这个”被需要“的状态怎么去触发呢?这就需要另外一个操作了-末端操作。 - 末端操作 - 在对集合进行操作的时候,大部分情况下,我们在意的只是结果,而不是中间过程。末端操作就是一个返回结果的操作,它的返回值不能是序列,必须是一个明确的结果,比如列表、数字、对象等表意明确的结果。末端操作一般都放在链式操作的末尾,在执行末端操作的时候,会去触发中间操作的延迟计算,也就是将”被需要“这个状态打开了,我们给上面的例子加上末端操作: + 在对集合进行操作的时候,大部分情况下,我们在意的只是结果,而不是中间过程。 + 末端操作就是一个返回结果的操作,它的返回值不能是序列,必须是一个明确的结果, + 比如列表、数字、对象等表意明确的结果。末端操作一般都放在链式操作的末尾, + 在执行末端操作的时候,会去触发中间操作的延迟计算,也就是将”被需要“这个状态打开了, + 我们给上面的例子加上末端操作: ```kotlin list.asSequence().filter { @@ -377,7 +394,11 @@ list.asSequence().filter {it > 2}.map {it * 2}.toList() [6, 8, 10] ``` - 可以看到,所有的中间操作都被执行了。从上面执行打印的结果我们发现,它的执行顺序与我们预想的不一样。普通集合在进行链式操作的时候会先在list上调用filter,然后产生一个结果列表,接下来map就在这个结果列表上进行操作。而序列则不一样,序列在执行链式操作的时候,会将所有的操作都应用在一个元素上,也就是说,第一个元素执行完所有的操作之后,第二个元素再去执行所有的操作,以此类推。放映到我们这个例子上面,就是第一个元素执行了filter之后再去执行map,然后第二个元素也是这样。 + 可以看到,所有的中间操作都被执行了。从上面执行打印的结果我们发现,它的执行顺序与我们预想的不一样。 + 普通集合在进行链式操作的时候会先在list上调用filter,然后产生一个结果列表,接下来map就在这个结果列表上进行操作。 + 而序列则不一样,序列在执行链式操作的时候,会将所有的操作都应用在一个元素上,也就是说,第一个元素执行完所有的操作之后, + 第二个元素再去执行所有的操作,以此类推。放到我们这个例子上面,就是第一个元素执行了filter之后再去执行map, + 然后第二个元素也是这样。 #### 序列可以是无限的 @@ -385,13 +406,16 @@ list.asSequence().filter {it > 2}.map {it * 2}.toList() 那接下来,该怎么去实现一个自然数数列呢?采用一般的列表肯定是不行的,因为构造一个列表必须列举出列表中的元素,而我们是没有办法将自然数全部列举出来的。 -我们知道,自然数是有一定规律的,就是最后一个数永远是前一个数加1的结果,我们只需要实现一个列表,让这个列表描述这种规律,那么也就相当于实现了一个无限的自然数数列。好看Kotlin也给我们提供了这样一个方法,去创建无限的数列: +我们知道,自然数是有一定规律的,就是最后一个数永远是前一个数加1的结果,我们只需要实现一个列表,让这个列表描述这种规律,那么也就相当于实现了一个无限的自然数数列。 +好在Kotlin也给我们提供了这样一个方法,去创建无限的数列: ```kotlin val naturalNumList = generateSequence(0) { it + 1} ``` -通过上面这一行代码,我们就非常简单的实现了自然数数列,上面我们调用了一个方法generateSequence来创建序列。我们知道序列是惰性求值的,所以上面创建的序列是不会把所有的自然数都列举出来的,只有在我们调用一个末端操作的时候,才去列举我们所需要的列表。比如我们要从这个自然数列表中取出前10个自然数: +通过上面这一行代码,我们就非常简单的实现了自然数数列,上面我们调用了一个方法generateSequence来创建序列。 +我们知道序列是惰性求值的,所以上面创建的序列是不会把所有的自然数都列举出来的,只有在我们调用一个末端操作的时候, +才去列举我们所需要的列表。比如我们要从这个自然数列表中取出前10个自然数: ```kotlin naturalNumList.takeWhile{it <= 9}.toList() @@ -399,9 +423,6 @@ naturalNumList.takeWhile{it <= 9}.toList() ``` - - - ### 可`null`类型 @@ -480,7 +501,7 @@ val email = data["email"] ?: throw IllegalStateException("Email is missing!") val l = b!!.length ``` -因此,如果你想要一个 NPE,你可以得到它,但是你必须显式要求它,否则它不会不期而至。 +因此,如果你想要一个NPE,你可以得到它,但是你必须显式要求它,否则它不会不期而至。 #### 安全的类型转换 @@ -540,9 +561,6 @@ loop@ for (i in 1..100) { ``` - - - [上一篇:Kotlin学习教程(二)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%8C).md) [下一篇:Kotlin学习教程(四)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%9B%9B).md) diff --git "a/KotlinCourse/4.Kotlin_\350\241\250\350\276\276\345\274\217&\345\205\263\351\224\256\345\255\227.md" "b/KotlinCourse/4.Kotlin_\350\241\250\350\276\276\345\274\217&\345\205\263\351\224\256\345\255\227.md" index 43178b4e..f2196189 100644 --- "a/KotlinCourse/4.Kotlin_\350\241\250\350\276\276\345\274\217&\345\205\263\351\224\256\345\255\227.md" +++ "b/KotlinCourse/4.Kotlin_\350\241\250\350\276\276\345\274\217&\345\205\263\351\224\256\345\255\227.md" @@ -1,4 +1,4 @@ -Kotlin学习教程(四) +4.Kotlin_表达式&关键字 === ## `if`表达式 @@ -18,8 +18,8 @@ val max = if (a > b) { ## `when`表达式 -`when`表达式与`Java`中的`switch/case`类似,但是要强大得多。这个表达式会去试图匹配所有可能的分支直到找到满意的一项。然后它会运行右边的表达 -式。 +`when`表达式与`Java`中的`switch/case`类似,但是要强大得多。这个表达式会去试图匹配所有可能的分支直到找到满意的一项。 +然后它会运行右边的表达式。 与`Java`的`switch/case`不同之处是参数可以是任何类型,并且分支也可以是一个条件。 对于默认的选项,我们可以增加一个`else`分支,它会在前面没有任何条件匹配时再执行。条件匹配成功后执行的代码也可以是代码块: @@ -90,7 +90,8 @@ fun schedule(sunny: Boolean, day: Day) = when (day) { } ``` -一个完整的when表达式类似switch语句,由when关键字开始,用花括号包含多个逻辑分支,每个分支由-> 连接,不再需要switch的break(这真是一个恼人的关键字),由上往下匹配,一直匹配完为止,否则执行else分支的逻辑,类似switch的default。 +一个完整的when表达式类似switch语句,由when关键字开始,用花括号包含多个逻辑分支,每个分支由-> 连接, +不再需要switch的break(这真是一个恼人的关键字),由上往下匹配,一直匹配完为止,否则执行else分支的逻辑,类似switch的default。 到这里你可能会说上面的例子中,这样嵌套子when表达式,层次依旧比较深。要知道when表达式是很灵活的,我们很容易通过如下修改来解决这个问题: @@ -179,7 +180,7 @@ for(num in nums) { -上面in、step、downTo、until这几个,他们可以不通过点号,而是通过中缀表达式来被调用,从而让语法变得更加简洁直观。 +上面in、step、downTo、until这几个,他们可以不通过点号,而是通过**中缀表达式**来被调用,从而让语法变得更加简洁直观。 @@ -187,8 +188,8 @@ for(num in nums) { - `var`:定义变量 - `val`:定义常量 -- `fun`:定义方法 -- `Unit`:默认方法返回值,类似于`Java`中的`void`,可以理解成返回没什么用的值 +- `fun`:定义函数 +- `Unit`:默认方法返回值,类似于`Java`中的`void`,可以理解成返回没什么用的值类型 - `vararg`:可变参数 - `$`:字符串模板(取值) - 位运算符:`or`(按位或),`and`(按位与),`shl`(有符号左移),`shr`(有符号右移), diff --git "a/KotlinCourse/5.Kotlin_\345\206\205\351\203\250\347\261\273&\345\257\206\345\260\201\347\261\273&\346\236\232\344\270\276&\345\247\224\346\211\230.md" "b/KotlinCourse/5.Kotlin_\345\206\205\351\203\250\347\261\273&\345\257\206\345\260\201\347\261\273&\346\236\232\344\270\276&\345\247\224\346\211\230.md" index 4bcc58cc..d6ea8ce6 100644 --- "a/KotlinCourse/5.Kotlin_\345\206\205\351\203\250\347\261\273&\345\257\206\345\260\201\347\261\273&\346\236\232\344\270\276&\345\247\224\346\211\230.md" +++ "b/KotlinCourse/5.Kotlin_\345\206\205\351\203\250\347\261\273&\345\257\206\345\260\201\347\261\273&\346\236\232\344\270\276&\345\247\224\346\211\230.md" @@ -1,4 +1,4 @@ -Kotlin学习教程(五) +5.Kotlin_内部类&密封类&枚举&委托 === @@ -97,9 +97,11 @@ outter.Inner().execute() #### 内部类vs嵌套类 -在Java中,我们通过在内部类的语法上增加一个static关键词,把它变成一个嵌套类。然而,Kotlin则是相反的思路,默认是一个嵌套类,必须加上inner关键字才是一个内部类,也就是说可以把静态的内部类看成嵌套类。 +在Java中,我们通过在内部类的语法上增加一个static关键词,把它变成一个嵌套类。然而,Kotlin则是相反的思路,默认是一个嵌套类, +必须加上inner关键字才是一个内部类,也就是说可以把静态的内部类看成嵌套类。 -内部类和嵌套类有明显的差别,具体体现在:内部类包含着对其外部类实例的引用,在内部类中我们可以使用外部类中的属性。而在嵌套类中不包含对其外部类实例的引用,所以它无法调用其外部类的属性。 +内部类和嵌套类有明显的差别,具体体现在: +- 内部类包含着对其外部类实例的引用,在内部类中我们可以使用外部类中的属性。而在嵌套类中不包含对其外部类实例的引用,所以它无法调用其外部类的属性。 @@ -180,7 +182,8 @@ sealed class Bird { } ``` -Kotlin通过sealed关键字来修饰一个类为密封类,若要继承则需要将子类定义在同一个文件中,其他文件中的类将无法继承他。但这种方式也有它的局限性。即它不能被初始化,因为它背后是基于一个抽象类实现的,这一点可以从它转换后的Java代码看出: +Kotlin通过sealed关键字来修饰一个类为密封类,若要继承则需要将子类定义在同一个文件中,其他文件中的类将无法继承他。 +但这种方式也有它的局限性。即它不能被初始化,因为它背后是基于一个抽象类实现的,这一点可以从它转换后的Java代码看出: ```java public abstract class Bird { @@ -206,7 +209,6 @@ public abstract class Bird { ``` - 密封类用来表示受限的类继承结构:当一个值为有限集中的 类型、而不能有任何其他类型时。在某种意义上,他们是枚举类的扩展:枚举类型的值集合 也是受限的,但每个枚举常量只存在一个实例,而密封类 @@ -279,9 +281,40 @@ val s = try { x as String } catch(e: ClassCastException) { null } ### 对象`(Object)` -在Java中,static是非常重要的特性,它可以用来修饰类、方法或属性。然而static修饰的内容都属于类的,而不是某个具体对象的,但在定义时却与普通的变量和方法混杂在一起,显得格格不入。 +在Java中,static是非常重要的特性,它可以用来修饰类、方法或属性。然而static修饰的内容都属于类的,而不是某个具体对象的, +但在定义时却与普通的变量和方法混杂在一起,显得格格不入。 + +在Kotlin中,你将告别static这种语法,因为它引入了全新的关键字object,可以完美的代替使用static的所有场景。 +当然除了代替static的场景之外,它还能实现更多的功能,比如单例对象及简化匿名表达式等。 + +声明对象就如同声明一个类,你只需要用保留字`object`替代`class`,其他都相同。只需要考虑到对象不能有构造函数,因为我们不调用任何构造函数来访问 +它们。事实上,对象就是具有单一实现的数据类型。object声明的内容可以看成没有构造方法的类,它会在系统或者类加载时进行初始化。 + +```kotlin +object Resource { + val name = "Name" +} +``` + +### 对象表达式 + +对象也能用于创建匿名类实现。 + +```java +recycler.adapter = object : RecyclerView.Adapter() { + override fun onBindViewHolder(holder: RecyclerView.ViewHolder?, position: Int) { + } + + override fun getItemCount(): Int { + } + + override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder { + } +} +``` +例如,每次想要创建一个接口的内联实现,或者扩展另一个类时,你将使用上面的符号。 -在Kotlin中,你将告别static这种语法,因为它引入了全新的关键字object,可以完美的代替使用static的所有场景。当然除了代替static的场景之外,它还能实现更多的功能,比如单例对象及简化匿名表达式等。 +object表达式和匿名内部类很像,那对象表达式与Lambda表达式哪个更适合代替匿名内部类呢? 当你的匿名内部类使用的类接口只需要实现一个方法时,使用Lambda表达式更适合。当匿名内部类内有多个方法实现的时候,使用object表达式更适合。 #### 伴生对象 @@ -313,11 +346,15 @@ public class Prize { } ``` -上面是很常见的Java代码,也许你已经习惯了,但是如果仔细思考,会发现这种语法其实并不是非常好。因为在一个类中既有静态变量、静态方法,也有普通变量、普通方法的声明。然而,静态变量和静态方法是属于一个类的,普通变量、普通方法是属于一个具体对象的。虽然有static作为区分,然而在代码结构上职能并不是区分得很清晰。 +上面是很常见的Java代码,也许你已经习惯了,但是如果仔细思考,会发现这种语法其实并不是非常好。因为在一个类中既有静态变量、静态方法, +也有普通变量、普通方法的声明。然而,静态变量和静态方法是属于一个类的,普通变量、普通方法是属于一个具体对象的。 +虽然有static作为区分,然而在代码结构上职能并不是区分得很清晰。 -那么,有没有一种方式能将这两部分代码清晰的分开,但又不失语义化呢?Kotlin中引入了伴生对象的概念,简单来说,这是一种利用companion object两个关键字创造的语法。 +那么,有没有一种方式能将这两部分代码清晰的分开,但又不失语义化呢?Kotlin中引入了伴生对象的概念,简单来说, +这是一种利用companion object两个关键字创造的语法。 -伴生对象:“伴生”是相较于一个类而言的,意为伴随某个类的对象,它属于这个类所有,因此伴生对象跟Java中static修饰效果性质一样,全局只有一个单例。它需要声明在类的内部,在类被装载时会被初始化。 +伴生对象:“伴生”是相较于一个类而言的,意为伴随某个类的对象,它属于这个类所有,因此伴生对象跟Java中static修饰效果性质一样, +全局只有一个单例。它需要声明在类的内部,在类被装载时会被初始化。 现在将上面的例子改写成伴生对象的版本: @@ -339,7 +376,8 @@ class Prize(val name: String, val count: Int, val type: Int) { } ``` -可以发现,该版本在语义上更清晰了。而且,companion object用花括号包裹了所有静态属性和方法,使得它可以与Prize类的普通方法和属性清晰的区分开来。最后,我们可以使用点号来对一个类的静态的成员进行调用。 +可以发现,该版本在语义上更清晰了。而且,companion object用花括号包裹了所有静态属性和方法,使得它可以与Prize类的普通 +方法和属性清晰的区分开来。最后,我们可以使用点号来对一个类的静态的成员进行调用。 伴生对象的另一个作用是可以实现工厂方法模式。前面也说过如何从构造方法实现工厂方法模式,然而这种方法存在以下缺点: @@ -370,19 +408,28 @@ class Prize private constructor(val name: String, val count: Int, val type: Int) } ``` -总的来说,伴生对象是Kotlin中用来代替static关键字的一种方式,任何在Java类内部用static定义的内容都可以用Kotlin中的伴生对象来实现。然而,他们是类似的,一个类的伴生对象跟一个静态类一样,全局只能有一个。 +总的来说,伴生对象是Kotlin中用来代替static关键字的一种方式,任何在Java类内部用static定义的内容都可以用Kotlin中的伴生对象来实现。 +然而,他们是类似的,一个类的伴生对象跟一个静态类一样,全局只能有一个。 - - -声明对象就如同声明一个类,你只需要用保留字`object`替代`class`,其他都相同。只需要考虑到对象不能有构造函数,因为我们不调用任何构造函数来访问 -它们。事实上,对象就是具有单一实现的数据类型。object声明的内容可以看成没有构造方法的类,它会在系统或者类加载时进行初始化。 - -```kotlin -object Resource { - val name = "Name" +每个类都可以实现一个伴生对象,它是该类的所有实例共有的对象。它将类似于`Java`中的静态字段。 +```java +class App : Application() { + companion object { + lateinit var instance: App + private set + } + + override fun onCreate() { + super.onCreate() + instance = this + } } ``` +在这例子中,创建一个由`Application`扩展的(派送)的类,并且在`companion object`中存储它的唯一实例。 +`lateinit`表示这个属性开始是没有值得,但是,在使用前将被赋值(否则,就会抛出异常)。 +`private set`用于说明外部类不能对其进行赋值。 + ### 单例 因为一个类的伴生对象跟一个静态类一样,全局只能有一个。这让我们联想到了什么? 没错,就是单例对象。 @@ -393,7 +440,8 @@ object Resource { } ``` -因为对象就是具有单一实现的数据类型,所以在`kotlin`中对象就是单例。 单例对象会在系统加载的时候初始化,当然全局只有一个,所以它是饿汉式的单例。 +因为对象就是具有单一实现的数据类型,所以在`kotlin`中对象就是单例。 +单例对象会在系统加载的时候初始化,当然全局只有一个,所以它是饿汉式的单例。 看一下自动生成的java代码: @@ -439,50 +487,6 @@ public static Instance getInstance() { 具体可以看: [Kotlin Object Declarations Are Initialized in Static Block](https://hanru-yeh.medium.com/kotlin-object-declarations-are-initialized-in-static-block-5e1c2e1c3401) -### 对象表达式 - -对象也能用于创建匿名类实现。 - -```java -recycler.adapter = object : RecyclerView.Adapter() { - override fun onBindViewHolder(holder: RecyclerView.ViewHolder?, position: Int) { - } - - override fun getItemCount(): Int { - } - - override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder { - } -} -``` -例如,每次想要创建一个接口的内联实现,或者扩展另一个类时,你将使用上面的符号。 - -object表达式和匿名内部类很像,那对象表达式与Lambda表达式哪个更适合代替匿名内部类呢? 当你的匿名内部类使用的类接口只需要实现一个方法时,使用Lambda表达式更适合。当匿名内部类内有多个方法实现的时候,使用object表达式更适合。 - - - - -### 伴生对象`(Companion Object)` - -每个类都可以实现一个伴生对象,它是该类的所有实例共有的对象。它将类似于`Java`中的静态字段。 -```java -class App : Application() { - companion object { - lateinit var instance: App - private set - } - - override fun onCreate() { - super.onCreate() - instance = this - } -} -``` - -在这例子中,创建一个由`Application`扩展的(派送)的类,并且在`companion object`中存储它的唯一实例。 -`lateinit`表示这个属性开始是没有值得,但是,在使用前将被赋值(否则,就会抛出异常)。 -`private set`用于说明外部类不能对其进行赋值。 - ### 委托(代理) diff --git "a/KotlinCourse/6.Kotlin_\345\244\232\347\273\247\346\211\277\351\227\256\351\242\230.md" "b/KotlinCourse/6.Kotlin_\345\244\232\347\273\247\346\211\277\351\227\256\351\242\230.md" index 7017a62f..29bd7403 100644 --- "a/KotlinCourse/6.Kotlin_\345\244\232\347\273\247\346\211\277\351\227\256\351\242\230.md" +++ "b/KotlinCourse/6.Kotlin_\345\244\232\347\273\247\346\211\277\351\227\256\351\242\230.md" @@ -1,4 +1,4 @@ -Kotlin学习教程之多继承问题 +6.Kotlin_多继承问题 === @@ -191,16 +191,6 @@ fun main(args: Array) { - - - - - - - - - - [上一篇:Kotlin学习教程(七)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%83).md) [下一篇:Kotlin学习教程(八)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AB).md) diff --git "a/KotlinCourse/7.Kotlin_\346\263\250\350\247\243&\345\217\215\345\260\204&\346\211\251\345\261\225.md" "b/KotlinCourse/7.Kotlin_\346\263\250\350\247\243&\345\217\215\345\260\204&\346\211\251\345\261\225.md" index fdbd19e2..42330826 100644 --- "a/KotlinCourse/7.Kotlin_\346\263\250\350\247\243&\345\217\215\345\260\204&\346\211\251\345\261\225.md" +++ "b/KotlinCourse/7.Kotlin_\346\263\250\350\247\243&\345\217\215\345\260\204&\346\211\251\345\261\225.md" @@ -1,4 +1,4 @@ -Kotlin学习教程(六) +7.Kotlin_注解&反射&扩展 === ### 注解 @@ -79,9 +79,20 @@ val numbers = listOf(1, 2, 3) println(numbers.filter(::isOdd)) // 输出 [1, 3] ``` -### 扩展 +## 扩展 -扩展是`kotlin`中非常重要的一个特性,它能让我们对一些已有的类进行功能增加、简化,使他们更好的应对我们的需求。 +扩展是`kotlin`中非常重要的一个特性,扩展函数允许我们在不改变已有类的情况下,为类添加新的函数。 +扩展函数是指对类的方法进行扩展,写法和定义方法类似,但是要声明目标类,也就是对哪个类进行扩展,`kotlin`中称之为`Top Level`。 +扩展函数表现得就像是属于这个类的一样,而且我们可以使用`this`关键字和调用所有`public`方法。 +扩展函数可以在已有类中添加新的方法,不会对原类做修改,扩展函数定义形式: +```kotlin +fun receiverType.functionName(params){ + body +} +receiverType:表示函数的接收者,也就是函数扩展的对象 +functionName:扩展函数的名称 +params:扩展函数的参数,可以为NULL +``` ```kotlin // 对Context的扩展,增加了toast方法。为了更好的看到效果,我还加了一段log日志 @@ -102,25 +113,10 @@ class MainActivity : AppCompatActivity() { Toast msg : hello, Extension ``` -按照通常的做法,会写一个`ToastUtils`工具类,或者在`BaseActivity`中实现`toast`。但是使用扩展函数就会简单很。 -上面的例子就是在`Context`中添加新的方法,让我们以更简单的方式去显示`toast`,并且不用传入任何`context`参数,可以被任何`Context`或者 -它的子类调用。我们可以在任何地方(例如一个工具类文件中)声明这个函数,然后在`Activity`中将它作为普通方法来直接调用。当然了`Anko`中已经包括了 -自己的`toast`扩展函数。有关`Anko`后面会讲到。 - -`Kotlin`扩展函数允许我们在不改变已有类的情况下,为类添加新的函数。 -扩展函数是指对类的方法进行扩展,写法和定义方法类似,但是要声明目标类,也就是对哪个类进行扩展,`kotlin`中称之为`Top Level`。 -扩展函数表现得就像是属于这个类的一样,而且我们可以使用`this`关键字和调用所有`public`方法。 -扩展函数可以在已有类中添加新的方法,不会对原类做修改,扩展函数定义形式: -```kotlin -fun receiverType.functionName(params){ - body -} -receiverType:表示函数的接收者,也就是函数扩展的对象 -functionName:扩展函数的名称 -params:扩展函数的参数,可以为NULL -``` - -在上面我们举的扩展的例子就是扩展函数.其中`Context`就是目标类`Top Level`,我们把它放到方法名前,用点`.`表示从属关系。在方法体中用关键字 +按照通常的做法,会写一个`ToastUtils`工具类,或者在`BaseActivity`中实现`toast`。但是使用扩展函数就会简单很多。 +上面的例子就是在`Context`中添加新的方法,让我们以更简单的方式去显示`toast`,并且不用传入任何`context`参数, +可以被任何`Context`或者它的子类调用。我们可以在任何地方(例如一个工具类文件中)声明这个函数,然后在`Activity`中将 +它作为普通方法来直接调用。其中`Context`就是目标类`Top Level`,我们把它放到方法名前,用点`.`表示从属关系。在方法体中用关键字 `this`对本体进行调用。和普通方法一样,如果有返回值,在方法后面跟上返回类型,我这里没有返回值,所以直接省略了。 扩展函数的原理: 通过查看对应的Java代码可以发现: @@ -135,8 +131,6 @@ fun MutableList.exchange(fromIndex: Int, toIndex: Int) { } ``` - - ```java package com.st.stplayer.extension; @@ -162,10 +156,8 @@ public final class ExtenndsKt { } ``` -通过上面的Java代码可以看出,我们可以将扩展函数理解为静态方法。而静态方法的特点是:它独立于该类的任何对象,且不依赖类的特定实例,被该类的所有实例共享。此外,被public修饰的静态方法本质上也就是全局方法。所以扩展函数不会带来额外的性能消耗。 - - - +通过上面的Java代码可以看出,我们可以将扩展函数理解为静态方法。而静态方法的特点是:它独立于该类的任何对象,且不依赖类的特定实例, +被该类的所有实例共享。此外,被public修饰的静态方法本质上也就是全局方法。所以扩展函数不会带来额外的性能消耗。 ##### 扩展函数的作用域 @@ -175,7 +167,6 @@ public final class ExtenndsKt { ```kotlin package com.charon.ext -... val View.isVisible: Boolean get() = visibility == View.VISIBLE @@ -204,7 +195,8 @@ fun View.toBitmap(): Bitmap? { } ``` -我们知道在同一个包内是可以直接调用exchange方法的。如果需要在其他包中调用,只需要import相应的方法即可,这与调用Java全局静态方法类似。除此之外,实际开发时我们也可能会将扩展函数定义在一个Class内部统一管理: +我们知道在同一个包内是可以直接调用exchange方法的。如果需要在其他包中调用,只需要import相应的方法即可,这与调用Java全局静态方法类似。 +除此之外,实际开发时我们也可能会将扩展函数定义在一个Class内部统一管理: ```kotlin class Extends { @@ -216,7 +208,8 @@ class Extends { } ``` -当扩展函数定义在Extends类内部时,情况就与之前不一样了,这个时候你会发现,之前的exchange方法无法调用了(之前调用位置在Extends类外部)。借助IDEA我们可以查看到它对应的Java代码,这里展示关键部分: +当扩展函数定义在Extends类内部时,情况就与之前不一样了,这个时候你会发现,之前的exchange方法无法调用了(之前调用位置在Extends类外部)。 +借助IDEA我们可以查看到它对应的Java代码,这里展示关键部分: ```java public static final class Extends { @@ -241,7 +234,7 @@ class Son { } ``` -它包含一个成员方法foo(),加入我们哪天心血来潮,想对这个方法做特殊实现,利用扩展函数可能会写出如下代码: +它包含一个成员方法foo(),假如哪天心血来潮,想对这个方法做特殊实现,利用扩展函数可能会写出如下代码: ```kotlin fun Son.foo() = println("son called extension foo") @@ -254,9 +247,11 @@ object Test { } ``` -在我们的预期中,我们希望调用的是扩展函数foo(),但是输出的结果为: son called member foo。这表明当扩展函数和现有类的成员方法同时存在时,Kotlin将会默认使用类的成员方法。看起来似乎不够合理,并且很容易引发一些问题:我定义了新的方法,为什么还是调用了旧的方法? +在我们的预期中,我们希望调用的是扩展函数foo(),但是输出的结果为: son called member foo。这表明当扩展函数和现有类的成员方法同时存在时, +Kotlin将会默认使用类的成员方法。看起来似乎不够合理,并且很容易引发一些问题:我定义了新的方法,为什么还是调用了旧的方法? -但是换一个角度来思考,在多人开发的时候,如果每个人都对Son扩展了foo方法,是不是很容易造成混淆。对于第三方类库来说甚至是一场灾难:我们把不应该更改的方法改变了。所以在使用时,我们必须注意: 同名的类成员方法的优先级总高于扩展函数。 +但是换一个角度来思考,在多人开发的时候,如果每个人都对Son扩展了foo方法,是不是很容易造成混淆。对于第三方类库来说甚至是一场灾难: +我们把不应该更改的方法改变了。所以在使用时,我们必须注意:**同名的类成员方法的优先级总高于扩展函数**。 @@ -293,7 +288,7 @@ fun ImageView.loadImage(url: String) { } ``` -这样在调用的时候,不仅省去了更多的参数,而且ImageView的声明周期也得到了保证。 +这样在调用的时候,不仅省去了更多的参数,而且ImageView的生命周期也得到了保证。 在实际项目中,我们还需要考虑网络请求框架替换及维护的问题,一般会对图片的请求框架进行二次封装: @@ -411,7 +406,8 @@ fun testFoo() { 这个范围函数本身似乎不是很有用。但是相比范围,还有一点不错的是,它返回范围内最后一个对象。 -例如现在有这么一个场景:用户点击领取新人奖励的按钮,如果用户此时没有登陆则弹出loginDialog,如果已经登录则弹出领取奖励的getNewAccountDialog。我们可以使用以下代码来处理这个逻辑: +例如现在有这么一个场景:用户点击领取新人奖励的按钮,如果用户此时没有登陆则弹出loginDialog, +如果已经登录则弹出领取奖励的getNewAccountDialog。我们可以使用以下代码来处理这个逻辑: ```kotlin run { @@ -419,8 +415,6 @@ run { }.show() ``` - - 2. apply `apply`函数是这样的,调用某对象的`apply`函数,在函数范围内,可以任意调用该对象的任意方法,并返回该对象. @@ -450,8 +444,6 @@ let函数和apply函数很像,唯一不同的是返回值。apply返回的是 public inline fun T.let(block: (T) -> R): R = block(this) ``` - - 如果对象的值不为空,则允许执行这个方法。返回值是函数里面最后一行,或者指定return,与run一样,它同样限制了变量的作用域。 ```kotlin @@ -513,7 +505,6 @@ class Kot { ``` - #### `sNullOrEmpty | isNullOrBlank` ```kotlin @@ -591,18 +582,6 @@ Hello world Hello world ``` -#### also - -also是kotlin 1.1版本中新加入的内容,它像是let和apply函数的加强版: - -```kotlin -public inline fun T.also(block: (T) -> Unit): T { - block(this); return this -} -``` - -与apply一致,它的返回值是该函数的接收者。 - ### 调度方式对扩展函数的影响 Kotlin是一种静态类型语言,我们创建的每个对象不仅具有运行时,还具有编译时类型。在使用扩展函数时,要清楚地了解静态和动态调度之间的区别。 @@ -649,10 +628,10 @@ public static void main(String[] args) { 在这种情况下,即使base本质上是Extended的实例,最终还是会执行Base的方法。 - #### 扩展函数始终静态调度 -可能你会好奇,这和扩展有什么关系?我们知道,扩展函数都有一个接收器(receiver),由于接收器实际上只是字节代码中编译方法的参数,因此你可以重载它。但不能覆盖它。这可能是成员和扩展函数之间最重要的区别:前者是动态调度的,后者总是静态调度的。 +可能你会好奇,这和扩展有什么关系?我们知道,扩展函数都有一个接收器(receiver),由于接收器实际上只是字节代码中编译方法的参数, +因此你可以重载它。但不能覆盖它。这可能是成员和扩展函数之间最重要的区别:前者是动态调度的,后者总是静态调度的。 为了方便理解,举一个例子: @@ -675,12 +654,10 @@ I'm Extended.foo! 由于只考虑了编译时类型,第一个打印将调用Base.foo(),而第二个打印将调用Extended.foo()。 - - - ## 用扩展函数封装Utils -在Java中,我们习惯将常用的代码放到对应的工具类中,例如ToastUtils、NetworkUtils、ImageLoaderUtils等。以NetworkUtils为例,该类中我们通常会放入Android经常需要使用的网络相关方法。比如,我们现在有一个判断手机网络是否可用的方法: +在Java中,我们习惯将常用的代码放到对应的工具类中,例如ToastUtils、NetworkUtils、ImageLoaderUtils等。 +以NetworkUtils为例,该类中我们通常会放入Android经常需要使用的网络相关方法。比如,我们现在有一个判断手机网络是否可用的方法: ```java public class NetworkUtils { @@ -703,13 +680,16 @@ public class NetworkUtils { boolean isConnected = NetworkUtils.isMobileConnected(context); ``` -虽然用起来比没有封装之前优雅了很多,但是每次都要传入context,造成的烦琐我们先不计较,重要是可能会让调用者忽视context和mobileNetwork间的强关系。作为代码的使用者,我们更希望在调用时省略NetworkUtils类名,并且让isMobileConnected可以看起来像context的一个属性或方法。我们期望的是下面这样的使用方式: +虽然用起来比没有封装之前优雅了很多,但是每次都要传入context,造成的烦琐我们先不计较,重要是可能会让调用者忽视context +和mobileNetwork间的强关系。作为代码的使用者,我们更希望在调用时省略NetworkUtils类名,并且让isMobileConnected可以 +看起来像context的一个属性或方法。我们期望的是下面这样的使用方式: ```java boolean isConnected = context.isMobileConnected(); ``` -由于Context是Android SDK自带的类,我们无法对其进行修改。在Java中目前只能通过继承Context新增静态成员方法来实现。但是在Kotlin中,我们通过扩展函数就能简单的实现: +由于Context是Android SDK自带的类,我们无法对其进行修改。在Java中目前只能通过继承Context新增静态成员方法来实现。 +但是在Kotlin中,我们通过扩展函数就能简单的实现: ```kotlin fun Context.isMobileConnected(): Boolean { @@ -727,23 +707,25 @@ fun Context.isMobileConnected(): Boolean { val isConnected = context.isMobileConnected(); ``` -值得一提的是,在Android中对Context的生命周期需要进行很好的把控。这里我们应该使用ApplicationContext,防止出现声明周期不一致导致的内存泄露或者其他问题。 +值得一提的是,在Android中对Context的生命周期需要进行很好的把控。这里我们应该使用ApplicationContext, +防止出现生命周期不一致导致的内存泄露或者其他问题。 ## 运算符重载 -  Kotlin允许我们对所有的运算符(+ - * / % ++ --),以及其他关键字进行重载,从而拓展这些运算符与关键字的用法。 +Kotlin允许我们对所有的运算符(+ - * / % ++ --),以及其他关键字进行重载,从而拓展这些运算符与关键字的用法。 语法:运算符重载使用的是operator关键字,在指定函数的前面加上operator关键字,就可以实现运算符重载了。 -指定函数,不同的运算符对应的重载函数是不同的,比如:+ 对应plus()  - 对应minus() +指定函数,不同的运算符对应的重载函数是不同的,比如:+ 对应plus() 、 - 对应minus() 可以在类中,对同一运算符进行多次重载,来满足不同的需求。 -运算符重载实际上是函数重载,本质上是对运算符函数的调用,从运算符到对应函数的映射过程由编译器完成。或者理解成是对已有的运算符赋予他们新的含义。重载的修饰符是operator。 +运算符重载实际上是函数重载,本质上是对运算符函数的调用,从运算符到对应函数的映射过程由编译器完成。 +或者理解成是对已有的运算符赋予他们新的含义。 举例: -比如我们的+号,它的含义是两个数值相加: 1+1 = 2。 +号对应的函数名是plus。我们可以对+号这个函数进行重载,让它实现减法的效果。 +比如我们的+号,它的含义是两个数值相加: 1 + 1 = 2。 +号对应的函数名是plus。我们可以对+号这个函数进行重载,让它实现减法的效果。 下面我们实现一个Person数据类,然后重载Int的 + 号运算符,让一个Int对象可以直接和Person对象相加。返回的结果是这个Int对象减去Person对象的age的值。 @@ -846,6 +828,243 @@ fun main() { +#### 函数引用 + +当我们有一个命名函数声明如下: + +```kotlin +fun isOdd(x: Int) = x % 2 != 0 +``` + +我们可以很容易地直接调用它`(isOdd(5))`,但是我们也可以把它作为一个值传递。例如传给另一个函数。 +为此,我们使用`::`操作符: + +```kotlin +val numbers = listOf(1, 2, 3) +println(numbers.filter(::isOdd)) // 输出 [1, 3] +``` + +这里`::isOdd`是函数类型`(Int) -> Boolean`的一个值。 + +当上下文中已知函数期望的类型时`::`可以用于重载函数。 + +例如: + +```kotlin +fun isOdd(x: Int) = x % 2 != 0 +fun isOdd(s: String) = s == "brillig" || s == "slithy" || s == "tove" + +val numbers = listOf(1, 2, 3) +println(numbers.filter(::isOdd)) // 引用到 isOdd(x: Int) +``` + +或者,你可以通过将方法引用存储在具有显式指定类型的变量中来提供必要的上下文: + +```kotlin +val predicate: (String) -> Boolean = ::isOdd // 引用到 isOdd(x: String) +``` + +如果我们需要使用类的成员函数或扩展函数,它需要是限定的。 +例如`String::toCharArray`为类型`String`提供了一个扩展函数:`String.() -> CharArray`。 + + +#### 属性引用 + +要把属性作为`Kotlin`中的一等对象来访问,我们也可以使用`::`运算符: + +```kotlin +var x = 1 + +fun main(args: Array) { + println(::x.get()) // 输出 "1" + ::x.set(2) + println(x) // 输出 "2" +} +``` + +表达式`::x`求值为`KProperty`类型的属性对象,它允许我们使用 +`get()`读取它的值,或者使用`name`属性来获取属性名。更多信息请参见 +关于`KProperty`类的文档。 + +对于可变属性,例如`var y = 1`,`::y`返回`KMutableProperty`类型的一个值, +该类型有一个`set()`方法。 + +属性引用可以用在不需要参数的函数处: + +```kotlin +val strs = listOf("a", "bc", "def") +println(strs.map(String::length)) // 输出 [1, 2, 3] +``` + +要访问属于类的成员的属性,我们这样限定它: + +```kotlin +class A(val p: Int) + +fun main(args: Array) { + val prop = A::p + println(prop.get(A(1))) // 输出 "1" +} +``` + + +## Kotlin类型别名 + +类型别名为现有类型提供替代名称。 +如果类型名称太长,你可以另外引入较短的名称,并使用新的名称替代原类型名。 + +它有助于缩短较长的泛型类型。 +例如,通常缩减集合类型是很有吸引力的: + +```kotlin +typealias NodeSet = Set + +typealias FileTable = MutableMap> +``` + +你可以为函数类型提供另外的别名: + +```kotlin +typealias MyHandler = (Int, String, Any) -> Unit + +typealias Predicate = (T) -> Boolean +``` + +你可以为内部类和嵌套类创建新名称: + +```kotlin +class A { + inner class Inner +} +class B { + inner class Inner +} + +typealias AInner = A.Inner +typealias BInner = B.Inner +``` + + +类型别名不会引入新类型。 +它们等效于相应的底层类型。 +当你在代码中添加`typealias Predicate`并使用`Predicate`时,`Kotlin`编译器总是把它扩展为`(Int) -> Boolean`。 +因此,当你需要泛型函数类型时,你可以传递该类型的变量,反之亦然: + +```kotlin +typealias Predicate = (T) -> Boolean + +fun foo(p: Predicate) = p(42) + +fun main(args: Array) { + val f: (Int) -> Boolean = { it > 0 } + println(foo(f)) // 输出 "true" + + val p: Predicate = { it > 0 } + println(listOf(1, -2).filter(p)) // 输出 "[1]" +} +``` + + +## 文档 + +用来编写`Kotlin`代码文档的语言(相当于`Java`的`JavaDoc`)称为`KDoc`。本质上`KDoc`是将`JavaDoc`的块标签`(block tags)`语法( +扩展为支持`Kotlin`的特定构造)和`Markdown`的内联标记`(inline markup)`结合在一起。 + + +#### 生成文档 + +`Kotlin`的文档生成工具称为[Dokka](https://github.com/Kotlin/dokka)。 + +`Dokka`有`Gradle`、`Maven`和`Ant`的插件,因此你可以将文档生成集成到你的构建过程中。 + + +像`JavaDoc`一样,`KDoc`注释也以`/**`开头、以`*/`结尾。注释的每一行可以以 +星号开头,该星号不会当作注释内容的一部分。 + +按惯例来说,文档文本的第一段(到第一行空白行结束)是该元素的 +总体描述,接下来的注释是详细描述。 + +每个块标签都以一个新行开始且以`@`字符开头。 + +以下是使用`KDoc`编写类文档的一个示例: + +```kotlin +/** + * 一组*成员*。 + * + * 这个类没有有用的逻辑; 它只是一个文档示例。 + * + * @param T 这个组中的成员的类型。 + * @property name 这个组的名称。 + * @constructor 创建一个空组。 + */ +class Group(val name: String) { + /** + * 将 [member] 添加到这个组。 + * @return 这个组的新大小。 + */ + fun add(member: T): Int { …… } +} +``` + +`KDoc`目前支持以下块标签`(block tags)`: + +- `@param` <名称> + + 用于函数的值参数或者类、属性或函数的类型参数。 + 为了更好地将参数名称与描述分开,如果你愿意,可以将参数的名称括在 + 方括号中。因此,以下两种语法是等效的: + + ```kotlin + @param name 描述。 + @param[name] 描述。 + ``` + +- `@return` + + 用于函数的返回值。 + +- `@constructor` + + 用于类的主构造函数。 + +- `@receiver` + + 用于扩展函数的接收者。 + +- `@property` <名称> + + 用于类中具有指定名称的属性。这个标签可用于在 + 主构造函数中声明的属性,当然直接在属性定义的前面放置`doc`注释会很别扭。 + +- `@throws` <类>,`@exception` <类> + + 用于方法可能抛出的异常。因为`Kotlin`没有受检异常,所以也没有期望所有可能的异常都写文档,但是当它会为类的用户提供有用的信息时,仍然可以使用这个标签。 + +- `@sample` <标识符> + + 将具有指定限定的名称的函数的主体嵌入到当前元素的文档中,以显示如何使用该元素的示例。 + +- `@see` <标识符> + + 将到指定类或方法的链接添加到文档的另请参见块。 + +- @author + + 指定要编写文档的元素的作者。 + +- `@since` + + 指定要编写文档的元素引入时的软件版本。 + +- `@suppress` + + 从生成的文档中排除元素。可用于不是模块的官方`API`的一部分但还是必须在对外可见的元素。 + +`KDoc`不支持`@deprecated`这个标签。作为替代,请使用`@Deprecated`注解。 + + + [上一篇:Kotlin学习教程(五)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%94).md) diff --git "a/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" "b/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" index d732bbaf..4826886f 100644 --- "a/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" +++ "b/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" @@ -3,20 +3,330 @@ Kotlin引入了协程(Coroutine)来支持更好的异步操作,利用它我们可以避免在异步编程中使用大量的回调,同时相比传统多线程技术,它更容易提升系统的高并发处理能力。 -一些`API`启动长时间运行的操作(例如网络`IO`、文件`IO`、`CPU`或`GPU`密集型任务等),并要求调用者阻塞直到它们完成。协程提供了一种避免阻塞线程 -并用更廉价、更可控的操作替代线程阻塞的方法:协程挂起。 -协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、 -订阅相关事件、在不同线程(甚至不同机器!)上调度执行,而代码则保持如同顺序执行一样简单。 +### 起源 +协程是一个无优先级的子程序调用组件,允许子程序在特定的地方挂起恢复。线程包含于进程,协程包含于线程。只要内存足够, +一个线程中可以有任意多个协程,但某一时刻只能有一个协程在运行,多个协程分享该线程分配到的计算机资源。 +线程是由操作系统来进行调度的,当操作系统切换线程的时候,会产生一定的消耗。而协程不一样,协程是包含于线程的,也就是说协程 +是工作在线程之上的,协程的切换可以由程序自己来控制,不需要操作系统进行调度。这样的话就大大降低了开销。 -### 起源 -协程是一个无优先级的子程序调用组件,允许子程序在特定的地方挂起恢复。线程包含于进程,协程包含于线程。只要内存足够,一个线程中可以有任意多个协程,但某一时刻只能有一个协程在运行,多个协程分享该线程分配到的计算机资源。 -线程是由操作系统来进行调度的,当操作系统切换线程的时候,会产生一定的消耗。而协程不一样,协程是包含于线程的,也就是说协程是工作在线程之上的,协程的切换可以由程序自己来控制,不需要操作系统进行调度。这样的话就大大降低了开销。 +## 使用 + +如需在 Android 项目中使用协程,请将以下依赖项添加到应用的build.gradle文件中: + +```groovy +dependencies { + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' +} +``` + +## 示例 + +```kotlin +import kotlinx.coroutines.* + +fun main() { + GlobalScope.launch { // 在后台启动一个新的协程并继续 + delay(1000L) // 非阻塞的等待 1 秒钟(默认时间单位是毫秒) + println("World!") // 在延迟后打印输出 + } + println("Hello,") // 协程已在等待时主线程还在继续 + Thread.sleep(2000L) // 阻塞主线程 2 秒钟来保证 JVM 存活 +} +// 运行结果 +Hello, +World! +``` + +本质上,协程是轻量级的线程。它们在某些CoroutineScope上下文中与launch协程构建器一起启动。 +这里我们在ClobalScope中启动了一个新的协程,这意味着新协程的生命周期只受整个应用程序的生命周期限制。 + +```kotlin +import kotlinx.coroutines.* + +fun main() { + GlobalScope.launch { // 在后台启动一个新的协程并继续 + delay(1000L) + println("World!") + } + println("Hello,") // 主线程中的代码会立即执行 + runBlocking { // 但是这个表达式阻塞了主线程 + delay(2000L) // ……我们延迟 2 秒来保证 JVM 的存活 + } +} +``` + +调用了runBlocking的主线程会一直阻塞直到runBlocking内部的协程执行完毕。 + +这个示例可以使用更合乎惯用法的方式重写,使用runBlocking来包装main函数的执行: + +```kotlin +import kotlinx.coroutines.* + +fun main() = runBlocking { // 开始执行主协程 + GlobalScope.launch { // 在后台启动一个新的协程并继续 + delay(1000L) + println("World!") + } + println("Hello,") // 主协程在这里会立即执行 + delay(2000L) // 延迟 2 秒来保证 JVM 存活 +} +``` + +这里的runBlocking { …… }作为用来启动顶层主协程的适配器。 我们显式指定了其返回类型Unit, +因为在Kotlin中main函数必须返回Unit类型。runBlocking为最高级的协程,也就是主协程,launch创建的协程能够在 +runBlocking中运行(反过来是不行的)。所以上面的代码可以看做是在一个线程中创建了一个主协程,然后在主协程中创建了一个输出为“World!”的子协程。 + +### 等待一个作业 + +上面我们使用delay(2000L)来让主线程延迟2秒,保证JVM的存活,但是这样并不是一个好的实现方案,因为在很多情况下我们并不知道耗时的任务要执行多久。 + +```kotlin +val job = GlobalScope.launch { // 启动一个新协程并保持对这个作业的引用 + delay(1000L) + println("World!") +} +println("Hello,") +job.join() // 等待直到子协程执行结束 +``` + +加了job.join()后,程序就会一直等待,直到我们启动的协程结束。注意,这里的等待是非堵塞式的等待,不会将当前线程挂起。 + +### 结构化的并发 + +协程的实际使用还有一些需要改进的地方。 当我们使用 `GlobalScope.launch` 时,我们会创建一个顶层协程。虽然它很轻量, +但它运行时仍会消耗一些内存资源。如果我们忘记保持对新启动的协程的引用,它还会继续运行。如果协程中的代码挂起了会怎么样 +(例如,我们错误地延迟了太长时间),如果我们启动了太多的协程并导致内存不足会怎么样? +必须手动保持对所有已启动协程的引用并 [join](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/join.html) 之很容易出错。 + +有一个更好的解决办法。我们可以在代码中使用结构化并发。 我们可以在执行操作所在的指定作用域内启动协程, 而不是像通常使用线程 +(线程总是全局的)那样在 [GlobalScope](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/index.html) 中启动。 + +在我们的示例中,我们使用 [runBlocking](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html) +协程构建器将 `main` 函数转换为协程。 包括 `runBlocking` 在内的每个协程构建器都将 [CoroutineScope](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html) +的实例添加到其代码块所在的作用域中。 我们可以在这个作用域中启动协程而无需显式 `join` 之,因为外部协程(示例中的 `runBlocking`) +直到在其作用域中启动的所有协程都执行完毕后才会结束。因此,可以将我们的示例简化为: + +```kotlin +import kotlinx.coroutines.* + +fun main() = runBlocking { // this: CoroutineScope + launch { // 在 runBlocking 作用域中启动一个新协程 + delay(1000L) + println("World!") + } + println("Hello,") +} +``` + + +### 作用域构建器 + +除了由不同的构建器提供协程作用域之外,还可以使用 [coroutineScope](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html) +构建器声明自己的作用域。它会创建一个协程作用域并且在所有已启动子协程执行完毕之前不会结束。 + +[runBlocking](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html) 与 [coroutineScope](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html) +可能看起来很类似,因为它们都会等待其协程体以及所有子协程结束。 +主要区别在于,[runBlocking](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html) +方法会*阻塞*当前线程来等待, 而 [coroutineScope](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html) +只是挂起,会释放底层线程用于其他用途。 由于存在这点差异, +[runBlocking](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html) 是常规函数, +而 [coroutineScope](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html) 是挂起函数。 + +可以通过以下示例来演示: + +```kotlin +import kotlinx.coroutines.* + +fun main() = runBlocking { // this: CoroutineScope + launch { + delay(200L) + println("Task from runBlocking") + } + + coroutineScope { // 创建一个协程作用域 + launch { + delay(500L) + println("Task from nested launch") + } + + delay(100L) + println("Task from coroutine scope") // 这一行会在内嵌 launch 之前输出 + } + + println("Coroutine scope is over") // 这一行在内嵌 launch 执行完毕后才输出 +} +// 执行结果 +Task from coroutine scope +Task from runBlocking +Task from nested launch +Coroutine scope is over +``` + +### 提取函数重构 + +我们来将 `launch { …… }` 内部的代码块提取到独立的函数中。当你对这段代码执行“提取函数”重构时,你会得到一个带有 `suspend` 修饰符的新函数。 +这是你的第一个*挂起函数*。在协程内部可以像普通函数一样使用挂起函数, 不过其额外特性是,同样可以使用其他挂起函数(如本例中的 `delay`)来*挂起*协程的执行。 + +```kotlin +import kotlinx.coroutines.* + +fun main() = runBlocking { + launch { doWorld() } + println("Hello,") +} + +// 这是你的第一个挂起函数 +suspend fun doWorld() { + delay(1000L) + println("World!") +} + +// 执行结果 +Hello, +World! +``` + +### 取消协程的执行 + +在一个长时间运行的应用程序中,你也许需要对你的后台协程进行细粒度的控制。 比如说,一个用户也许关闭了一个启动了协程的界面, +那么现在协程的执行结果已经不再被需要了,这时,它应该是可以被取消的。 +该 [launch](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html) +函数返回了一个可以被用来取消运行中的协程的 [Job](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html): + +```kotlin +val job = launch { + repeat(1000) { i -> + println("job: I'm sleeping $i ...") + delay(500L) + } +} +delay(1300L) // 延迟一段时间 +println("main: I'm tired of waiting!") +job.cancel() // 取消该作业 +job.join() // 等待作业执行结束 +println("main: Now I can quit.") +// 执行结果 +job: I'm sleeping 0 ... +job: I'm sleeping 1 ... +job: I'm sleeping 2 ... +main: I'm tired of waiting! +main: Now I can quit. +``` + +一旦main函数调用了 `job.cancel`,我们在其它的协程中就看不到任何输出,因为它被取消了。 这里也有一个可以使 +[Job](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html) +挂起的函数 [cancelAndJoin](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/cancel-and-join.html) +它合并了对 [cancel](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/cancel.html) +以及 [join](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/join.html) 的调用。 + + + +### 取消是协作的 + +协程的取消是*协作*的。一段协程代码必须协作才能被取消。 所有 `kotlinx.coroutines` 中的挂起函数都是 *可被取消的* 。 +它们检查协程的取消,并在取消时抛出 [CancellationException](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-cancellation-exception/index.html)。 +然而,如果协程正在执行计算任务,并且没有检查取消的话,那么它是不能被取消的,就如如下示例代码所示: + +```kotlin +val startTime = System.currentTimeMillis() +val job = launch(Dispatchers.Default) { + var nextPrintTime = startTime + var i = 0 + while (i < 5) { // 一个执行计算的循环,只是为了占用 CPU + // 每秒打印消息两次 + if (System.currentTimeMillis() >= nextPrintTime) { + println("job: I'm sleeping ${i++} ...") + nextPrintTime += 500L + } + } +} +delay(1300L) // 等待一段时间 +println("main: I'm tired of waiting!") +job.cancelAndJoin() // 取消一个作业并且等待它结束 +println("main: Now I can quit.") +// 执行结果 +job: I'm sleeping 0 ... +job: I'm sleeping 1 ... +job: I'm sleeping 2 ... +main: I'm tired of waiting! +job: I'm sleeping 3 ... +job: I'm sleeping 4 ... +main: Now I can quit. +``` + +运行示例代码,并且我们可以看到它连续打印出了“I'm sleeping”,甚至在调用取消后, 作业仍然执行了五次循环迭代并运行到了它结束为止。 + +### 使计算代码可取消 + +我们有两种方法来使执行计算的代码可以被取消。第一种方法是定期调用挂起函数来检查取消。 +对于这种目的 [yield](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/yield.html) 是一个好的选择。 +另一种方法是显式的检查取消状态。让我们试试第二种方法。 + +将前一个示例中的 `while (i < 5)` 替换为 `while (isActive)` 并重新运行它。 + +```kotlin +val startTime = System.currentTimeMillis() +val job = launch(Dispatchers.Default) { + var nextPrintTime = startTime + var i = 0 + while (isActive) { // 可以被取消的计算循环 + // 每秒打印消息两次 + if (System.currentTimeMillis() >= nextPrintTime) { + println("job: I'm sleeping ${i++} ...") + nextPrintTime += 500L + } + } +} +delay(1300L) // 等待一段时间 +println("main: I'm tired of waiting!") +job.cancelAndJoin() // 取消该作业并等待它结束 +println("main: Now I can quit.") +// 执行结果 +job: I'm sleeping 0 ... +job: I'm sleeping 1 ... +job: I'm sleeping 2 ... +main: I'm tired of waiting! +main: Now I can quit. +``` + +你可以看到,现在循环被取消了。[isActive](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/is-active.html) +是一个可以被使用在 [CoroutineScope](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html) 中的扩展属性。 + + +### 在 `finally` 中释放资源 + +我们通常使用如下的方法处理在被取消时抛出 [CancellationException](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-cancellation-exception/index.html) +的可被取消的挂起函数。比如说,`try {……} finally {……}` 表达式以及 Kotlin 的 `use` 函数一般在协程被取消的时候执行它们的终结动作: + +```kotlin +val job = launch { + try { + repeat(1000) { i -> + println("job: I'm sleeping $i ...") + delay(500L) + } + } finally { + println("job: I'm running finally") + } +} +delay(1300L) // 延迟一段时间 +println("main: I'm tired of waiting!") +job.cancelAndJoin() // 取消该作业并且等待它结束 +println("main: Now I can quit.") +// 执行结果 +job: I'm sleeping 0 ... +job: I'm sleeping 1 ... +job: I'm sleeping 2 ... +main: I'm tired of waiting! +job: I'm running finally +main: Now I can quit. +``` ### 阻塞 vs 挂起 @@ -29,7 +339,7 @@ Kotlin引入了协程(Coroutine)来支持更好的异步操作,利用它 另一个区别是,协程不能在随机的指令中挂起,而只能在所谓的挂起点挂起,这会调用特别标记的函数。 -#### 挂起函数 +#### 挂起函数 当我们调用标记有特殊修饰符`suspend`的函数时,会发生挂起: @@ -38,7 +348,6 @@ suspend fun doSomething(foo: Foo): Bar { …… } ``` - 这样的函数称为挂起函数,因为调用它们可能挂起协程(如果相关调用的结果已经可用,库可以决定继续进行而不挂起)。挂起函数能够以与普通函数相同的方式 获取参数和返回值,但它们只能从协程和其他挂起函数中调用。事实上,要启动协程, 必须至少有一个挂起函数,它通常是匿名的(即它是一个挂起`lambda`表达式)。让我们来看一个例子,一个简化的`async()`函数 @@ -68,7 +377,6 @@ async { ``` -更多关于`async/await`函数实际在`kotlinx.coroutines`中如何工作的信息可以在这里找到。 请注意,挂起函数`await()`和`doSomething()`不能在像`main()`这样的普通函数中调用: ```kotlin @@ -76,7 +384,6 @@ fun main(args: Array) { doSomething() // 错误:挂起函数从非协程上下文调用 } ``` - 还要注意的是,挂起函数可以是虚拟的,当覆盖它们时,必须指定`suspend`修饰符: ```kotlin interface Base { @@ -88,6 +395,379 @@ class Derived: Base { } ``` +### 默认顺序调用 + +假设我们在不同的地方定义了两个进行某种调用远程服务或者进行计算的挂起函数。我们只假设它们都是有用的,但是实际上它们在这个示例中只是为了该目的而延迟了一秒钟: + +```kotlin +suspend fun doSomethingUsefulOne(): Int { + delay(1000L) // 假设我们在这里做了一些有用的事 + return 13 +} + +suspend fun doSomethingUsefulTwo(): Int { + delay(1000L) // 假设我们在这里也做了一些有用的事 + return 29 +} +``` + +如果需要按 *顺序* 调用它们,我们接下来会做什么——首先调用 `doSomethingUsefulOne` *接下来* 调用 `doSomethingUsefulTwo`,并且计算它们结果的和吗? +实际上,如果我们要根据第一个函数的结果来决定是否我们需要调用第二个函数或者决定如何调用它时,我们就会这样做。 + +我们使用普通的顺序来进行调用,因为这些代码是运行在协程中的,只要像常规的代码一样 *顺序* 都是默认的。 +下面的示例展示了测量执行两个挂起函数所需要的总时间: + +```kotlin +val time = measureTimeMillis { + val one = doSomethingUsefulOne() + val two = doSomethingUsefulTwo() + println("The answer is ${one + two}") +} +println("Completed in $time ms") +``` + +它的打印输出如下: + +``` +The answer is 42 +Completed in 2017 ms +``` + +### 使用 async 并发 + +如果 `doSomethingUsefulOne` 与 `doSomethingUsefulTwo` 之间没有依赖,并且我们想更快的得到结果,让它们进行 *并发* 吗? +这就是 [async](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html) 可以帮助我们的地方。 + +在概念上,[async](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html) +就类似于 [launch](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html)。 +它启动了一个单独的协程,这是一个轻量级的线程并与其它所有的协程一起并发的工作。不同之处在于 `launch` 返回一个 +[Job](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html) 并且不附带任何结果值, +而 `async` 返回一个 [Deferred](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html) —— +一个轻量级的非阻塞可取消的future,这代表了一个将会在稍后提供结果的 promise。你可以使用 `.await()` 在一个延期的值上得到它的最终结果, +但是 `Deferred` 也是一个 `Job`,所以如果需要的话,你可以取消它。 + +```kotlin +val time = measureTimeMillis { + val one = async { doSomethingUsefulOne() } + val two = async { doSomethingUsefulTwo() } + println("The answer is ${one.await() + two.await()}") +} +println("Completed in $time ms") +``` +它的打印输出如下: + +``` +The answer is 42 +Completed in 1017 ms +``` + +这里快了两倍,因为两个协程并发执行。请注意,使用协程进行并发总是显式的。 + +### 惰性启动的 async + +可选的,[async](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html) +可以通过将 `start` 参数设置为 [CoroutineStart.LAZY](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-start/-l-a-z-y.html) 而变为惰性的。 +在这个模式下,只有结果通过 [await](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/await.html) 获取的时候协程才会启动, +或者在 `Job` 的 [start](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/start.html) 函数调用的时候。运行下面的示例: + +```kotlin +val time = measureTimeMillis { + val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() } + val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() } + // 执行一些计算 + one.start() // 启动第一个 + two.start() // 启动第二个 + println("The answer is ${one.await() + two.await()}") +} +println("Completed in $time ms") +``` +它的打印输出如下: + +``` +The answer is 42 +Completed in 1017 ms +``` + +因此,在先前的例子中这里定义的两个协程没有执行,但是控制权在于程序员准确的在开始执行时调用 +[start](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/start.html)。 +我们首先 调用 `one`,然后调用 `two`,接下来等待这个协程执行完毕。 + +注意,如果我们只是在 `println` 中调用 [await](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/await.html), +而没有在单独的协程中调用 [start](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/start.html),这将会导致顺序行为, +直到 [await](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/await.html) 启动该协程 执行并等待至它结束, +这并不是惰性的预期用例。 在计算一个值涉及挂起函数时,这个 `async(start = CoroutineStart.LAZY)` 的用例用于替代标准库中的 `lazy` 函数。 + + + +## 协程上下文与调度器 + +协程总是运行在一些以 [CoroutineContext](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines/-coroutine-context/) +类型为代表的上下文中,它们被定义在了 Kotlin 的标准库里。 + +协程上下文是各种不同元素的集合。其中主元素是协程中的 [Job](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html), +我们在前面的文档中见过它以及它的调度器,而本文将对它进行介绍。 + +### 调度器与线程 + +协程上下文包含一个 *协程调度器* (参见 [CoroutineDispatcher](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/index.html)) +它确定了相关的协程在哪个线程或哪些线程上执行。协程调度器可以将协程限制在一个特定的线程执行,或将它分派到一个线程池,亦或是让它不受限地运行。 + +所有的协程构建器诸如 [launch](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html) +和 [async](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html) 接收一个可选的 +[CoroutineContext](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines/-coroutine-context/) 参数,它可以被用来显式的为一个新协程或其它上下文元素指定一个调度器。 + +尝试下面的示例: + +```kotlin +launch { // 运行在父协程的上下文中,即 runBlocking 主协程 + println("main runBlocking : I'm working in thread ${Thread.currentThread().name}") +} +launch(Dispatchers.Unconfined) { // 不受限的——将工作在主线程中 + println("Unconfined : I'm working in thread ${Thread.currentThread().name}") +} +launch(Dispatchers.Default) { // 将会获取默认调度器 + println("Default : I'm working in thread ${Thread.currentThread().name}") +} +launch(newSingleThreadContext("MyOwnThread")) { // 将使它获得一个新的线程 + println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}") +} +``` + +``` +Unconfined : I'm working in thread main +Default : I'm working in thread DefaultDispatcher-worker-1 +newSingleThreadContext: I'm working in thread MyOwnThread +main runBlocking : I'm working in thread main +``` + +当调用 `launch { …… }` 时不传参数,它从启动了它的 [CoroutineScope](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html) +中承袭了上下文(以及调度器)。在这个案例中,它从 `main` 线程中的 `runBlocking` 主协程承袭了上下文。 + +[Dispatchers.Unconfined](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-unconfined.html) +是一个特殊的调度器且似乎也运行在 `main` 线程中,但实际上, 它是一种不同的机制,这会在后文中讲到。 + +当协程在 [GlobalScope](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/index.html) 中启动时, +使用的是由 [Dispatchers.Default](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html) 代表的默认调度器。 +默认调度器使用共享的后台线程池。 所以 `launch(Dispatchers.Default) { …… }` 与 `GlobalScope.launch { …… }` 使用相同的调度器。 + +[newSingleThreadContext](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/new-single-thread-context.html) +为协程的运行启动了一个线程。 一个专用的线程是一种非常昂贵的资源。 在真实的应用程序中两者都必须被释放,当不再需要的时候, +使用 [close](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-executor-coroutine-dispatcher/close.html) 函数, +或存储在一个顶层变量中使它在整个应用程序中被重用。 + + + +### 非受限调度器 vs 受限调度器 + +[Dispatchers.Unconfined](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-unconfined.html) +协程调度器在调用它的线程启动了一个协程,但它仅仅只是运行到第一个挂起点。挂起后,它恢复线程中的协程,而这完全由被调用的挂起函数来决定。 +非受限的调度器非常适用于执行不消耗 CPU 时间的任务,以及不更新局限于特定线程的任何共享数据(如UI)的协程。 + +另一方面,该调度器默认继承了外部的 [CoroutineScope](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html)。 +[runBlocking](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html) 协程的默认调度器, +特别是, 当它被限制在了调用者线程时,继承自它将会有效地限制协程在该线程运行并且具有可预测的 FIFO 调度。 + +``` +launch(Dispatchers.Unconfined) { // 非受限的——将和主线程一起工作 + println("Unconfined : I'm working in thread ${Thread.currentThread().name}") + delay(500) + println("Unconfined : After delay in thread ${Thread.currentThread().name}") +} +launch { // 父协程的上下文,主 runBlocking 协程 + println("main runBlocking: I'm working in thread ${Thread.currentThread().name}") + delay(1000) + println("main runBlocking: After delay in thread ${Thread.currentThread().name}") +} +``` + +执行后的输出: + +``` +Unconfined : I'm working in thread main +main runBlocking: I'm working in thread main +Unconfined : After delay in thread kotlinx.coroutines.DefaultExecutor +main runBlocking: After delay in thread main +``` + +所以,该协程的上下文继承自 `runBlocking {...}` 协程并在 `main` 线程中运行, +当 [delay](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/delay.html) 函数调用的时候, +非受限的那个协程在默认的执行者线程中恢复执行。 + +> 非受限的调度器是一种高级机制,可以在某些极端情况下提供帮助而不需要调度协程以便稍后执行或产生不希望的副作用, 因为某些操作必须立即在协程中执行。 非受限调度器不应该在通常的代码中使用。 + + + +### 命名协程以用于调试 + +当协程经常打印日志并且你只需要关联来自同一个协程的日志记录时, +则自动分配的 id 是非常好的。然而,当一个协程与特定请求的处理相关联时或做一些特定的后台任务, +最好将其明确命名以用于调试目的。 [CoroutineName](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-name/index.html) +上下文元素与线程名具有相同的目的。当[调试模式](http://www.kotlincn.net/docs/reference/coroutines/coroutine-context-and-dispatchers.html#调试协程与线程)开启时, +它被包含在正在执行此协程的线程名中。 + +下面的例子演示了这一概念: + +```kotlin +log("Started main coroutine") +// 运行两个后台值计算 +val v1 = async(CoroutineName("v1coroutine")) { + delay(500) + log("Computing v1") + 252 +} +val v2 = async(CoroutineName("v2coroutine")) { + delay(1000) + log("Computing v2") + 6 +} +log("The answer for v1 / v2 = ${v1.await() / v2.await()}") +``` + +程序执行使用了 `-Dkotlinx.coroutines.debug` JVM 参数,输出如下所示: + +``` +[main @main#1] Started main coroutine +[main @v1coroutine#2] Computing v1 +[main @v2coroutine#3] Computing v2 +[main @main#1] The answer for v1 / v2 = 42 +``` + + +### 组合上下文中的元素 + +有时我们需要在协程上下文中定义多个元素。我们可以使用 `+` 操作符来实现。 比如说,我们可以显式指定一个调度器来启动协程 +并且同时显式指定一个命名: + +```kotlin +launch(Dispatchers.Default + CoroutineName("test")) { + println("I'm working in thread ${Thread.currentThread().name}") +} +``` + +这段代码使用了 `-Dkotlinx.coroutines.debug` JVM 参数,输出如下所示: + +``` +I'm working in thread DefaultDispatcher-worker-1 @test#2 +``` + + + +### 协程作用域 + +让我们将关于上下文,子协程以及作业的知识综合在一起。假设我们的应用程序拥有一个具有生命周期的对象,但这个对象并不是一个协程。 +举例来说,我们编写了一个 Android 应用程序并在 Android 的 activity 上下文中启动了一组协程来使用异步操作拉取并更新数据以及执行动画等等。 +所有这些协程必须在这个 activity 销毁的时候取消以避免内存泄漏。当然,我们也可以手动操作上下文与作业,以结合 activity 的生命周期与它的协程, +但是 `kotlinx.coroutines` 提供了一个封装:[CoroutineScope](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html) 的抽象。 +你应该已经熟悉了协程作用域,因为所有的协程构建器都声明为在它之上的扩展。 + +我们通过创建一个 [CoroutineScope](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html) 实例来管理协程的生命周期, +并使它与 activity 的生命周期相关联。`CoroutineScope` 可以通过 [CoroutineScope()](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope.html) +创建或者通过[MainScope()](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-main-scope.html) 工厂函数。 +前者创建了一个通用作用域,而后者为使用 [Dispatchers.Main](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-main.html) +作为默认调度器的 UI 应用程序 创建作用域: + +``` +class Activity { + private val mainScope = MainScope() + + fun destroy() { + mainScope.cancel() + } + // 继续运行…… +``` + +现在,我们可以使用定义的 `scope` 在这个 `Activity` 的作用域内启动协程。 +对于该示例,我们启动了十个协程,它们会延迟不同的时间: + +``` +// 在 Activity 类中 + fun doSomething() { + // 在示例中启动了 10 个协程,且每个都工作了不同的时长 + repeat(10) { i -> + mainScope.launch { + delay((i + 1) * 200L) // 延迟 200 毫秒、400 毫秒、600 毫秒等等不同的时间 + println("Coroutine $i is done") + } + } + } +} // Activity 类结束 +``` + +在 main 函数中我们创建 activity,调用测试函数 `doSomething`,并且在 500 毫秒后销毁这个 activity。 +这取消了从 `doSomething` 启动的所有协程。我们可以观察到这些是由于在销毁之后, 即使我们再等一会儿,activity 也不再打印消息。 + +``` +val activity = Activity() +activity.doSomething() // 运行测试函数 +println("Launched coroutines") +delay(500L) // 延迟半秒钟 +println("Destroying activity!") +activity.destroy() // 取消所有的协程 +delay(1000) // 为了在视觉上确认它们没有工作 +``` + +这个示例的输出如下所示: + +``` +Launched coroutines +Coroutine 0 is done +Coroutine 1 is done +Destroying activity! +``` + +你可以看到,只有前两个协程打印了消息,而另一个协程在 `Activity.destroy()` 中单次调用了 `job.cancel()`。 + + +## 通道 + +延期的值提供了一种便捷的方法使单个值在多个协程之间进行相互传输。 通道提供了一种在流中传输值的方法。 + +### 通道基础 + +一个 [Channel](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-channel/index.html) +是一个和 `BlockingQueue` 非常相似的概念。其中一个不同是它代替了阻塞的 `put` 操作并提供了挂起的 +[send](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/send.html), +还替代了阻塞的 `take` 操作并提供了挂起的 [receive](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/receive.html)。 + +``` +val channel = Channel() +launch { + // 这里可能是消耗大量 CPU 运算的异步逻辑,我们将仅仅做 5 次整数的平方并发送 + for (x in 1..5) channel.send(x * x) +} +// 这里我们打印了 5 次被接收的整数: +repeat(5) { println(channel.receive()) } +println("Done!") +``` +这段代码的输出如下: + +``` +1 +4 +9 +16 +25 +Done! +``` + +### 关闭与迭代通道 + +和队列不同,一个通道可以通过被关闭来表明没有更多的元素将会进入通道。 在接收者中可以定期的使用 `for` 循环来从通道中接收元素。 + +从概念上来说,一个 [close](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/close.html) +操作就像向通道发送了一个特殊的关闭指令。 这个迭代停止就说明关闭指令已经被接收了。所以这里保证所有先前发送出去的元素都在通道关闭前被接收到。 + +``` +val channel = Channel() +launch { + for (x in 1..5) channel.send(x * x) + channel.close() // 我们结束发送 +} +// 这里我们使用 `for` 循环来打印所有被接收到的元素(直到通道被关闭) +for (y in channel) println(y) +println("Done!") +``` + diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\271\235).md" b/KotlinCourse/9.Kotlin_androidktx.md similarity index 56% rename from "KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\271\235).md" rename to KotlinCourse/9.Kotlin_androidktx.md index 1a484128..aaef520a 100644 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\271\235).md" +++ b/KotlinCourse/9.Kotlin_androidktx.md @@ -1,139 +1,6 @@ -Kotlin学习教程(九) +9.Kotlin_androidktx === - -`Kotlin`团队为`Android`开发提供了一套超越标准语言功能的工具: - -- `Kotlin Android Extensions`是一个编译器扩展,可以让您摆脱代码中的`findViewById()`调用,并将其替换为合成编译器生成的属性。 -- `Anko`是一个提供围绕`Android API`和`DSL`的一组`Kotlin`友好的包装器,可以用`Kotlin`代码替换`layout .xml`文件。 - - -### `Anko` - -[Anko](https://github.com/Kotlin/anko)是`Kotlin`官方出品用于`Android`开发的库。 - - -> `Anko is a Kotlin library which makes Android application development faster and easier. It makes your code clean and -> easy to read, and lets you forget about rough edges of the Android SDK for Java.` - - -> Anko consists of several parts: -> -> Anko Commons: a lightweight library full of helpers for intents, dialogs, logging and so on; - > Intents; - > Dialogs and toasts; - > Logging; - > Resources and dimensions; -> Anko Layouts: a fast and type-safe way to write dynamic Android layouts; -> Anko SQLite: a query DSL and parser collection for Android SQLite; -> Anko Coroutines: utilities based on the kotlinx.coroutines library. - -`Anko`使用`DSL`提供了很多便捷的功能,可以直接用代码去写布局,不过我还是接受不了,感觉用`xml`写布局把布局和逻辑区分开挺好,这里就不介绍了,想用的可以去看看。 - -这里就用几个简单的`commons`中的例子,平时想要启动另一个`activity`我们经常这样写: -```kotlin -val intent = Intent(this, SomeOtherActivity::class.java) -intent.putExtra("id", 5) -intent.setFlag(Intent.FLAG_ACTIVITY_SINGLE_TOP) -startActivity(intent) -``` -这里要四行代码,太多了,`anko`提供了更简单的方式: -```kotlin -startActivity(intentFor("id" to 5).singleTop()) -``` -如果不设置`flag`的话还可以这样写: -```kotlin -startActivity("id" to 5) -``` - -而且`anko`还提供了一些常用的`intent`的封装功能: - -- 打电话`makeCall(number) without tel` -- 发短信`sendSMS(number, [text]) without sms` -- 浏览网页`browse(url)` -- 分享`share(text, [subject])` -- 发邮件`email(email, [subject], [text])` - -进行`toast`和`snakebar`提示: -```kotlin -toast("Hi there!") -toast(R.string.message) -longToast("Wow, such duration") - -snackbar(view, "Hi there!") -snackbar(view, R.string.message) -longSnackbar(view, "Wow, such duration") -snackbar(view, "Action, reaction", "Click me!") { doStuff() } -``` - -对话框: -```kotlin -alert("Hi, I'm Roy", "Have you tried turning it off and on again?") { - yesButton { toast("Oh…") } - noButton {} -}.show() - -// 列表对话框 -val countries = listOf("Russia", "USA", "Japan", "Australia") -selector("Where are you from?", countries, { dialogInterface, i -> - toast("So you're living in ${countries[i]}, right?") -}) - -// 进度对话框 -val dialog = progressDialog(message = "Please wait a bit…", title = "Fetching data") -``` - -`log`: -```kotlin -class SomeActivity : Activity(), AnkoLogger { - private fun someMethod() { - info("London is the capital of Great Britain") - debug(5) // .toString() method will be executed - warn(null) // "null" will be printed - } -} -``` -或者: -```kotlin -class SomeActivity : Activity() { - private val log = AnkoLogger(this) - private val logWithASpecificTag = AnkoLogger("my_tag") - - private fun someMethod() { - log.warning("Big brother is watching you!") - } -} -``` - -`dimens`: - -可以直接使用`px2dip`和`px2sp`来进行尺寸转换。 - - -异步操作: -```kotlin -doAsync { - // Long background task - uiThread { - result.text = "Done" - } -} -``` - -使用`anko`: -```kotlin -// 创建一个verticallayout并且添加edittext和button,并给button设置点击事件 -verticalLayout { - val name = editText() - button("Say Hello") { - onClick { toast("Hello, ${name.text}!") } - } -} -``` - - -### Kotlin Android Extensions - 相信每一位安卓开发人员对`findViewById()`这个方法再熟悉不过了,毫无疑问,潜在的`bug`和脏乱的代码令后续开发无从下手的。 尽管存在一系列的 开源库能够为这个问题带来解决方案,然而对于运行时依赖的库,需要为每一个`View`注解变量字段。 @@ -227,31 +94,6 @@ public class ForecastRequest(val zipCode: String) { ``` -### 创建Application - -```kotlin -class App : Application() { - companion object { - private var instance: Application? = null - fun instance() = instance!! - } - override fun onCreate() { - super.onCreate() - instance = this - } -} - -``` - - - - -- with() - -```kotlin -inline fun with(t: T, body: T.() -> Unit) { t.body() } -``` - [上一篇:Kotlin学习教程(八)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AB).md) diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\253).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\253).md" deleted file mode 100644 index 6ed5d235..00000000 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\253).md" +++ /dev/null @@ -1,460 +0,0 @@ -Kotlin学习教程(八) -=== - -`Kotlin`协程 ---- - -Kotlin引入了协程(Coroutine)来支持更好的异步操作,利用它我们可以避免在异步编程中使用大量的回调,同时相比传统多线程技术,它 - -一些`API`启动长时间运行的操作(例如网络`IO`、文件`IO`、`CPU`或`GPU`密集型任务等),并要求调用者阻塞直到它们完成。协程提供了一种避免阻塞线程 -并用更廉价、更可控的操作替代线程阻塞的方法:协程挂起。 -协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、 -订阅相关事件、在不同线程(甚至不同机器!)上调度执行,而代码则保持如同顺序执行一样简单。 - - -### 阻塞 vs 挂起 - -基本上,协程计算可以被挂起而无需阻塞线程。线程阻塞的代价通常是昂贵的,尤其在高负载时,因为只有相对少量线程实际可用,因此阻塞其中一个会导致一些 -重要的任务被延迟。 - -另一方面,协程挂起几乎是无代价的。不需要上下文切换或者`OS`的任何其他干预。最重要的是,挂起可以在很大程度上由用户库控制: -作为库的作者,我们可以决定挂起时发生什么并根据需求优化/记日志/截获。 - -另一个区别是,协程不能在随机的指令中挂起,而只能在所谓的挂起点挂起,这会调用特别标记的函数。 - -#### 挂起函数 - -当我们调用标记有特殊修饰符`suspend`的函数时,会发生挂起: - -```kotlin -suspend fun doSomething(foo: Foo): Bar { - …… -} -``` - -这样的函数称为挂起函数,因为调用它们可能挂起协程(如果相关调用的结果已经可用,库可以决定继续进行而不挂起)。挂起函数能够以与普通函数相同的方式 -获取参数和返回值,但它们只能从协程和其他挂起函数中调用。事实上,要启动协程, -必须至少有一个挂起函数,它通常是匿名的(即它是一个挂起`lambda`表达式)。让我们来看一个例子,一个简化的`async()`函数 -(源自`kotlinx.coroutines`库): - -```kotlin -fun async(block: suspend () -> T) -``` - -这里的`async()`是一个普通函数(不是挂起函数),但是它的`block`参数具有一个带`suspend`修饰符的函数类型:`suspend() -> T`。 -所以,当我们将一个`lambda`表达式传给`async()`时,它会是挂起`lambda`表达式,于是我们可以从中调用挂起函数: - -```kotlin -async { - doSomething(foo) - …… -} -``` - -继续该类比,`await()`可以是一个挂起函数(因此也可以在一个`async {}`块中调用),该函数挂起一个协程,直到一些计算完成并返回其结果: -```kotlin -async { - …… - val result = computation.await() - …… -} - -``` - -更多关于`async/await`函数实际在`kotlinx.coroutines`中如何工作的信息可以在这里找到。 - -请注意,挂起函数`await()`和`doSomething()`不能在像`main()`这样的普通函数中调用: -```kotlin -fun main(args: Array) { - doSomething() // 错误:挂起函数从非协程上下文调用 -} -``` - -还要注意的是,挂起函数可以是虚拟的,当覆盖它们时,必须指定`suspend`修饰符: -```kotlin -interface Base { - suspend fun foo() -} - -class Derived: Base { - override suspend fun foo() { …… } -} -``` - -`Kotlin`解构声明 ---- - - -有时把一个对象解构成很多变量会很方便: - -```kotlin -val (name, age) = person -``` - -这种语法称为解构声明。 一个解构声明可以同时创建多个变量。 - -我们已经声明了两个新变量:`name`和`age`,并且可以独立使用它们: -```kotlin -println(name) -println(age) -``` - -一个解构声明会被编译成以下代码: -```kotlin -val name = person.component1() -val age = person.component2() -``` - -其中的`component1()`和`component2()`函数是在`Kotlin`中广泛使用的约定原则的另一个例子。 - -任何表达式都可以出现在解构声明的右侧,只要可以对它调用所需数量的`component`函数即可。 -当然,可以有`component3()`和`component4()`等等。 - -请注意,`componentN()`函数需要用`operator`关键字标记,以允许在解构声明中使用它们。 - -解构声明也可以用在`for{: .keyword }`循环中: -```kotlin -for ((a, b) in collection) { …… } -``` -变量`a`和`b`的值取自对集合中的元素上调用`component1()`和`component2()`的返回值。 - -例:从函数中返回两个变量 -让我们假设我们需要从一个函数返回两个东西。例如,一个结果对象和一个某种状态。 -在`Kotlin`中一个简洁的实现方式是声明一个数据类并返回其实例: -```kotlin -data class Result(val result: Int, val status: Status) -fun function(……): Result { - // 各种计算 - return Result(result, status) -} -// 现在,使用该函数: -val (result, status) = function(……) -``` -因为数据类自动声明`componentN()`函数,所以这里可以用解构声明。 - -注意:我们也可以使用标准类`Pair`并且让`function()`返回`Pair`, -但是让数据合理命名通常更好。 - -例:解构声明和映射 -可能遍历一个映射`(map)`最好的方式就是这样: -```kotlin -for ((key, value) in map) { - // 使用该 key、value 做些事情 -} -``` -为使其能用,我们应该 -通过提供一个`iterator()`函数将映射表示为一个值的序列, -通过提供函数`component1()`和`component2()`来将每个元素呈现为一对。 -当然事实上,标准库提供了这样的扩展: -```kotlin -operator fun Map.iterator(): Iterator> = entrySet().iterator() -operator fun Map.Entry.component1() = getKey() -operator fun Map.Entry.component2() = getValue() -``` -因此你可以在`for{: .keyword }`-循环中对映射(以及数据类实例的集合等)自由使用解构声明。 - - -`Kotlin`反射 ---- - - -最基本的反射功能是获取`Kotlin`类的运行时引用。要获取对 -静态已知的`Kotlin`类的引用,可以使用类字面值语法: -```kotlin -val c = KClass::class -``` - -该引用是`KClass`类型的值。 - -请注意,`Kotlin`类引用与`Java`类引用不同。要获得`Java`类引用, -请在`KClass`实例上使用`.java`属性,也就是`KClass::class.java` - - -#### 函数引用 - -当我们有一个命名函数声明如下: -```kotlin -fun isOdd(x: Int) = x % 2 != 0 -``` - -我们可以很容易地直接调用它`(isOdd(5))`,但是我们也可以把它作为一个值传递。例如传给另一个函数。 -为此,我们使用`::`操作符: -```kotlin -val numbers = listOf(1, 2, 3) -println(numbers.filter(::isOdd)) // 输出 [1, 3] -``` -这里`::isOdd`是函数类型`(Int) -> Boolean`的一个值。 - -当上下文中已知函数期望的类型时`::`可以用于重载函数。 - -例如: -```kotlin -fun isOdd(x: Int) = x % 2 != 0 -fun isOdd(s: String) = s == "brillig" || s == "slithy" || s == "tove" - -val numbers = listOf(1, 2, 3) -println(numbers.filter(::isOdd)) // 引用到 isOdd(x: Int) -``` - -或者,你可以通过将方法引用存储在具有显式指定类型的变量中来提供必要的上下文: -```kotlin -val predicate: (String) -> Boolean = ::isOdd // 引用到 isOdd(x: String) -``` -如果我们需要使用类的成员函数或扩展函数,它需要是限定的。 -例如`String::toCharArray`为类型`String`提供了一个扩展函数:`String.() -> CharArray`。 - - -#### 属性引用 - -要把属性作为`Kotlin`中的一等对象来访问,我们也可以使用`::`运算符: - -```kotlin -var x = 1 - -fun main(args: Array) { - println(::x.get()) // 输出 "1" - ::x.set(2) - println(x) // 输出 "2" -} -``` -表达式`::x`求值为`KProperty`类型的属性对象,它允许我们使用 -`get()`读取它的值,或者使用`name`属性来获取属性名。更多信息请参见 -关于`KProperty`类的文档。 - -对于可变属性,例如`var y = 1`,`::y`返回`KMutableProperty`类型的一个值, -该类型有一个`set()`方法。 - -属性引用可以用在不需要参数的函数处: -```kotlin -val strs = listOf("a", "bc", "def") -println(strs.map(String::length)) // 输出 [1, 2, 3] -``` -要访问属于类的成员的属性,我们这样限定它: -```kotlin -class A(val p: Int) - -fun main(args: Array) { - val prop = A::p - println(prop.get(A(1))) // 输出 "1" -} -``` - - -Kotlin类型别名 ---- - -类型别名为现有类型提供替代名称。 -如果类型名称太长,你可以另外引入较短的名称,并使用新的名称替代原类型名。 - -它有助于缩短较长的泛型类型。 -例如,通常缩减集合类型是很有吸引力的: -```kotlin -typealias NodeSet = Set - -typealias FileTable = MutableMap> -``` - -你可以为函数类型提供另外的别名: -```kotlin -typealias MyHandler = (Int, String, Any) -> Unit - -typealias Predicate = (T) -> Boolean -``` -你可以为内部类和嵌套类创建新名称: - -```kotlin -class A { - inner class Inner -} -class B { - inner class Inner -} - -typealias AInner = A.Inner -typealias BInner = B.Inner -``` - - -类型别名不会引入新类型。 -它们等效于相应的底层类型。 -当你在代码中添加`typealias Predicate`并使用`Predicate`时,`Kotlin`编译器总是把它扩展为`(Int) -> Boolean`。 -因此,当你需要泛型函数类型时,你可以传递该类型的变量,反之亦然: - -```kotlin -typealias Predicate = (T) -> Boolean - -fun foo(p: Predicate) = p(42) - -fun main(args: Array) { - val f: (Int) -> Boolean = { it > 0 } - println(foo(f)) // 输出 "true" - - val p: Predicate = { it > 0 } - println(listOf(1, -2).filter(p)) // 输出 "[1]" -} -``` - - - -文档 ---- - - -用来编写`Kotlin`代码文档的语言(相当于`Java`的`JavaDoc`)称为`KDoc`。本质上`KDoc`是将`JavaDoc`的块标签`(block tags)`语法( -扩展为支持`Kotlin`的特定构造)和`Markdown`的内联标记`(inline markup)`结合在一起。 - - -#### 生成文档 - -`Kotlin`的文档生成工具称为[Dokka](https://github.com/Kotlin/dokka)。 - -`Dokka`有`Gradle`、`Maven`和`Ant`的插件,因此你可以将文档生成集成到你的构建过程中。 - - -像`JavaDoc`一样,`KDoc`注释也以`/**`开头、以`*/`结尾。注释的每一行可以以 -星号开头,该星号不会当作注释内容的一部分。 - -按惯例来说,文档文本的第一段(到第一行空白行结束)是该元素的 -总体描述,接下来的注释是详细描述。 - -每个块标签都以一个新行开始且以`@`字符开头。 - -以下是使用`KDoc`编写类文档的一个示例: -```kotlin -/** - * 一组*成员*。 - * - * 这个类没有有用的逻辑; 它只是一个文档示例。 - * - * @param T 这个组中的成员的类型。 - * @property name 这个组的名称。 - * @constructor 创建一个空组。 - */ -class Group(val name: String) { - /** - * 将 [member] 添加到这个组。 - * @return 这个组的新大小。 - */ - fun add(member: T): Int { …… } -} -``` - -`KDoc`目前支持以下块标签`(block tags)`: - -- `@param` <名称> - - 用于函数的值参数或者类、属性或函数的类型参数。 - 为了更好地将参数名称与描述分开,如果你愿意,可以将参数的名称括在 - 方括号中。因此,以下两种语法是等效的: - ```kotlin - @param name 描述。 - @param[name] 描述。 - ``` - -- `@return` - - 用于函数的返回值。 - -- `@constructor` - - 用于类的主构造函数。 - -- `@receiver` - - 用于扩展函数的接收者。 - -- `@property` <名称> - - 用于类中具有指定名称的属性。这个标签可用于在 - 主构造函数中声明的属性,当然直接在属性定义的前面放置`doc`注释会很别扭。 - -- `@throws` <类>,`@exception` <类> - - 用于方法可能抛出的异常。因为`Kotlin`没有受检异常,所以也没有期望所有可能的异常都写文档,但是当它会为类的用户提供有用的信息时,仍然可以使用这个标签。 - -- `@sample` <标识符> - - 将具有指定限定的名称的函数的主体嵌入到当前元素的文档中,以显示如何使用该元素的示例。 - -- `@see` <标识符> - - 将到指定类或方法的链接添加到文档的另请参见块。 - -- @author - - 指定要编写文档的元素的作者。 - -- `@since` - - 指定要编写文档的元素引入时的软件版本。 - -- `@suppress` - - 从生成的文档中排除元素。可用于不是模块的官方`API`的一部分但还是必须在对外可见的元素。 - -`KDoc`不支持`@deprecated`这个标签。作为替代,请使用`@Deprecated`注解。 - - -#### 内联标记 - -对于内联标记,`KDoc`使用常规`Markdown`语法,扩展了支持用于链接到代码中其他元素的简写语法。 - -链接到元素 -要链接到另一个元素(类、方法、属性或参数),只需将其名称放在方括号中: -``` -为此目的,请使用方法 [foo]。 -``` -如果要为链接指定自定义标签(label),请使用 Markdown 引用样式语法: -``` -为此目的,请使用[这个方法][foo]。 -``` -你还可以在链接中使用限定的名称。请注意,与 JavaDoc 不同,限定的名称总是使用点字符 -来分隔组件,即使在方法名称之前: -``` -使用 [kotlin.reflect.KClass.properties] 来枚举类的属性。 -``` -链接中的名称与正写文档的元素内使用该名称使用相同的规则解析。 -特别是,这意味着如果你已将名称导入当前文件,那么当你在`KDoc`注释中使用它时, -不需要再对其进行完整限定。 - -请注意`KDoc`没有用于解析链接中的重载成员的任何语法。因为`Kotlin`文档生成 -工具将一个函数的所有重载的文档放在同一页面上,标识一个特定的重载函数 -并不是链接生效所必需的。 - - - -### Any - -我们都知道,Java并不能在真正意义上被称为一门“ 纯面向对象”语言,因为它的原始类型(如int)的值与函数等并不能被视作对象。 - -但是Kotlin不同,在Kotlin的类型系统中,并不区分原始类型(基本数据类型)和包装类型,我们使用的始终是同一个类型。虽然从严格意义上,我们不能说Kotlin是一门纯面向对象的语言,但它显然比Java有更纯的设计。 - - - -#### Any:非空类型的跟类型 - -与Object作为Java类层级结构的顶层类似,Any类型是Kotlin中所有非空类型(如String、Int)的超类,如: - -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/kotlin_any.png?raw=true) - -与Java不同的是,Kotlin不区分“原始类型”(primitive type)和其他的类型,他们都是同一类型层级结构的一部分。 如果定义了一个没有指定父类型的类型,则该类型将是Any的直接子类型。如: - -```kotlin -class Animal(val weight: Double) -``` - -#### Any?:所有类型的根类型 - -如果说Any是所有非空类型的根类型,那么Any?才是所有类型(可空和非空类型)的根类型。这也就是说?Any?是?Any的父类型。 - - - - -[上一篇:Kotlin学习教程(七)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%83).md) -[下一篇:Kotlin学习教程(八)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AB).md) - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\215\201).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\215\201).md" deleted file mode 100644 index 7e4012f9..00000000 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\215\201).md" +++ /dev/null @@ -1,13 +0,0 @@ -Kotlin学习教程(十) -=== - -- - - -[上一篇:Kotlin学习教程(九)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B9%9D).md) - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! diff --git "a/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/1.\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/1.\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" index 4e733af8..7964053a 100644 --- "a/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/1.\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" +++ "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/1.\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" @@ -47,8 +47,9 @@ - 比特率(码率) + 码率就是数据传输时单位时间传送的数据位数,一般我们用的单位是`kbps`即千位(bit)每秒,也就是每秒钟传送多少个千位的信息。 通俗一点的理解就是取样率,单位时间内取样率越大,精度就越高,处理出来的文件就越接近原始文件,但是文件体积与取样率是成正比的,所以几乎所有的编码格式重视的都是如何用最低的码率达到最少的失真。但是因为编码算法不一样,所以也不能用码率来统一衡量音质或者画质.小写的b表示bit(位),大写的B表示byte(字节),一个字节=8个位,即1B=8b;前面的k表示1024的意思,即1024个位(Kb)或者1024个字节(KB),表示文件的大小单位,一般使用KB。1KB/s=8Kbps。码率(kbps)=文件大小(KB)*8/时间(秒)。 - + - 动态码率(VBR: Variable Bit Rate) 比特率可以随着图像复杂程度的不同而随之变化。图像内容简单的片段采用较小的码率,图像 @@ -61,9 +62,10 @@ - 帧率(Frame Rate) + 帧/秒(`frames per second`)的缩写帧率即每秒显示帧数,帧率表示图形处理器处理场时每秒钟能够更新的次数。高的帧率可以得到更流畅、更逼真的动画。一般来说`30fps`就是可以接受的,但是将性能提升至`60fps`则可以明显提升交互感和逼真感,但是一般来说超过`75fps`一般就不容易察觉到有明显的流畅度提升了。如果帧率超过屏幕刷新率只会浪费图形处理的能力,因为监视器不能以这么快的速度更新,这样超过新率的帧率就浪费掉了。 ![image](https://github.com/CharonChui/Pictures/blob/master/fps.gif?raw=true) - + - 关键帧 相当于二维动画中的原画,指角色或者物体运动或变化中的关键动作所处的那一帧,它包含了图像的所有信息,后来帧仅包含了改变了的信息。如果你没有足够的关键帧,你的影片品质可能比较差,因为所有的帧从别的帧处产生。对于一般的用途,一个比较好的原则是每5秒设一个关键键。但如果时那种实时传输的流文件,那么要考虑传输网络的可靠度,所以要1到2秒增加一个关键帧。 @@ -210,7 +212,7 @@ YUV --- -YUV(也成YCbCr)是电视系统所采用的一种颜色编码方法,他是一种亮度与色度分离的色彩格式。其中Y表示明亮度也就是灰阶值,它是基础信号。U和V表示的则是色度,UV的作用是描述影像色彩及饱和度,它们用于指定像素的颜色。U和V不是基础信号,他俩都是被正交调制的。早期的电视都是黑白的,即只有亮度值(Y),有了彩色电视之后,加入了UV两种色度,形成现在的YUV,也叫YCbCr。人眼对亮度敏感,对色度不敏感,因此减少部分UV的数据量,人眼也无法感知出来,这样就可以通过压缩UV的分辨率,在不影响观感的前提下,减少视频的体积。 +YUV(也成YCbCr)是电视系统所采用的一种颜色编码方法,他是一种亮度与色度分离的色彩格式。其中Y表示明亮度也就是灰阶值,它是基础信号。U表示色度,V表示浓度,UV的作用是描述影像色彩及饱和度,它们用于指定像素的颜色。U和V不是基础信号,他俩都是被正交调制的。早期的电视都是黑白的,即只有亮度值(Y),有了彩色电视之后,加入了UV两种色度,形成现在的YUV,也叫YCbCr。人眼对亮度敏感,对色度不敏感,因此减少部分UV的数据量,人眼也无法感知出来,这样就可以通过压缩UV的分辨率,在不影响观感的前提下,减少视频的体积。 YUV和RGB视频信号相比,最大的优点在于只需要占用极少的带宽,YUV只需要占用RGB一般的带宽。 diff --git "a/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/11.\346\222\255\346\224\276\347\273\204\344\273\266\345\260\201\350\243\205.md" "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/11.\346\222\255\346\224\276\347\273\204\344\273\266\345\260\201\350\243\205.md" new file mode 100644 index 00000000..16479a5f --- /dev/null +++ "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/11.\346\222\255\346\224\276\347\273\204\344\273\266\345\260\201\350\243\205.md" @@ -0,0 +1,45 @@ +11.播放组件封装 +=== + +通常播放器的开发都设计成一定程度的分层,将视频帧的显示、进度条、控制键、音量调节、预览图、字幕、弹幕、频道列表、后续播放推荐等截面功能与音视频播放进行剥离,以使代码模块化,架构清晰。 + +为连接播放器截面和音视频播放,通常需设计一套状态机机制,音视频播放层需要负责包括解码器在内的软硬件初始化,搭建Pipeline以及进行播放控制。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/VideoDevelopment/\346\222\255\346\224\276\345\231\250\346\200\247\350\203\275\344\274\230\345\214\226.md" "b/VideoDevelopment/\346\222\255\346\224\276\345\231\250\346\200\247\350\203\275\344\274\230\345\214\226.md" index aebda409..b64ab96e 100644 --- "a/VideoDevelopment/\346\222\255\346\224\276\345\231\250\346\200\247\350\203\275\344\274\230\345\214\226.md" +++ "b/VideoDevelopment/\346\222\255\346\224\276\345\231\250\346\200\247\350\203\275\344\274\230\345\214\226.md" @@ -101,6 +101,41 @@ IP竞速 DNS解析加快,通常,DNS解析,意味着要把一个域名为xxx.com解析成ip过程,平时请求网页,网络差,就会打开网页半天。 +### 播放的关键指标:QOS +QOS(Quality of Service,服务质量)主要指网络环境下服务满足用户的程度,在视频服务的语境下也可认为是Quality of Streaming,即流媒体服务的质量。通常,QOS可以由一系列指标表达,如传输速度、响应时间、发送顺序、正确率等。就视频服务来说,QOS由多项约定俗成的技术指标构成,包括播放成功率、错误率、Re-buffer(卡顿)次数和时间、起始时间、快进响应时间、视频码率、延迟等。 +通行的QOS指标大致可分为两类: +- 一类用于衡量用户可在多大概率上得到服务,如播放成功率和错误率。 +- 另一类描述了用户所获取到服务的水平,如卡顿次数、时间、起始时间、快进时间、视频码率和延迟。 + +播放成功率描述了用户在尝试播放视频时启动成功的比率,可由所有成功开始播放的次数除以用户尝试的总数,常见于后端视频失效的情形。 +播放错误率意在针对播放过程中至少单个视频或音频帧被播放的情况下发生的错误,可能的原因包括播放器崩溃、硬件关闭、网络断开等,需要用户干预才能恢复播放。 +在视频服务质量日渐提升的今天,播放错误率出现的概率通常在千分之一以下甚至更低,用户最常见且容易不满的当属视频卡顿(也有人称之为缓冲率),即播放器无法即时得到流媒体传输的视频片段而需等待下载的情形。 +卡顿可能短程的发生,也可能持续很长时间,根据一些公司的研究,用户在观看视频点播时遇到一次以上的卡顿,会导致观看时间缩短一半,对直播用户的影响还要更甚于此。卡顿指标即包含单位时间内的卡顿次数也包含卡顿累计时间的维度,优化卡顿时间的最常见的方式是利用CDN和码率自适应算法。 + +视频卡顿的一类特殊情形是起始播放时的卡顿,通常计算从用户点击播放到第一帧呈现在屏幕上为止的时间长度,因为获取最初可用的视频片段需要一定时间,包括后台服务准备资源、下载视频开始的片段、初始化软硬件等。 +与播放过程中的卡顿不同,用户有等待数秒的心理预期,据调研2007年的大部分用户能接受10s以内的播放起始时间,但在2017年,5s的视频起始等待已被认为是非常糟糕的体验,中国许多视频服务商都提出了“秒开”的概念,力图将用户习以为常的起始时间固定在1s以内。另一类情形是快进时间,与起始时间非常类似,意指用户在点击快进后到视频呈现在屏幕之间的时间长度。 +优化起始时间可以通过将起始视频片段预先置于CDN的边缘节点,降低起始码率,增加播放器初始化并行度,预先建立网络连接等方式。此外,播放器还可以通过插入片头动画,持续播放快进前的视频片段直至快进后的视频帧准备好等手段降低用户的主观等待时间。 + +用户观看视频的平均码率也是一项核心指标,用于反馈视频的清晰度程度,针对直播服务,节目延迟时间也是核心指标,没有人原因在观看足球比赛时,隔壁已经为进球欢呼,而自己的电视上球员尚未开始射门,通常的计算标准是节目应播出的时间与实际屏幕上播放时间的间隔。带来延迟的除软件处理速度、网络传输速度外,编码器,源服务器及CDN服务器带来的缓存队列,播放器中解码器和渲染硬件均会引入大小不同的延迟。 + +QOS数据由后台服务整合后将被应用于图表呈现、统计报告、分析优化、监控报警等用途,是产品、开发、运维、数据分析等团队依靠的基础。 + +为更好的分析特定问题,收集关于某一用户播放过程的全部信息并按时序加以呈现,可以有效地帮助理解因果关系,信息将包括用户行为、执行时间、下载计时、码率切换记录、错误类型、CDN节点位置、服务器日志甚至一些计算的中间结果,将可有效地推断例如开始播放较为缓慢或者某次卡顿如何发生的原因。例如: +``` +xx:xx Player Seek Start +xx:xx Player Seek End +xx:xx New State: Buffering +.... +``` + +Conviva在2011年的分享中提到以下观点,缓冲次数始终是最主要的影响用户体验的因素,在观看90分钟长度的OTT直播中,增加1%的缓冲比率将使用户减少3分钟的观看时间,同时认为,在直播过程中,平均码率的影响要比点播中更大。 +在2016年的一篇文章中,研究者利用YouSlow的数据分析得到的看法是,缓冲对退出观看的影响比启动时间要高六倍。 +### QOE +视频公司在编码和传输上进行的优化,其指向的目标无疑是提升用户体验,或严格来讲,是获取更多的收入和利润。衡量编码或传输的优化表现,除了直接使用QOS指标如观看码率、启动时间、缓冲次数等,将其余QOE(Quality of Experience)指标连接则更可以直观地反应出成效。 +不断优化表现直接与收入关联的主要原因是,收入统计通常计出多门且较为滞后,可采用的替代指标包括注册用户数、活跃用户数、视频观察市场、观看次数、播放占比、付费率、留存率、分享率、满意度等。 + + + ### 监控 diff --git "a/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/HLS.md" "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/HLS.md" index 707b4758..b2f222a2 100644 --- "a/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/HLS.md" +++ "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/HLS.md" @@ -198,6 +198,14 @@ live m3u8文件列表需要不断更新,更新规则: +## TS相比MP4的劣势 + +TS文件虽然适于优先电视领域的应用,但在带宽使用上与DASH所使用的扩展格式MP4相比有巨大的劣势。首先TS包按188字节分割过于细小,其包头虽然仅占4字节,累积起来仍嫌浪费。其次,重复插入的PSI包对持续的视频播放而言颇显肿余(虽然HLS协议的后期版本通过EXT-X-MAP标签支持独立的仅含PSI包的TS文件,但将引入兼容问题)。更重要的是,由于TS包的字节数限制,当视频或音频包不足字节数时,需要加上许多无用的填充字节。据不同来源统计,三项合计,TS文件平均要浪费4%~13%的带宽,设计较好的封装格式,在寸带宽寸金的互联网世界中,意味着节约千万甚至上亿美元。 + + + + + #### 测试地址: From 63cf2e444cbae7c637aa8fabb4bfd594e477555b Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 8 Apr 2021 19:26:22 +0800 Subject: [PATCH 051/213] update README --- ...&\347\261\273&\346\216\245\345\217\243.md" | 5 ++- ...76\350\256\241\346\250\241\345\274\217.md" | 8 ++++ ...05\350\201\224\345\207\275\346\225\260.md" | 4 +- ...0\347\273\204&\351\233\206\345\220\210.md" | 4 +- ...7&\345\205\263\351\224\256\345\255\227.md" | 6 +++ ...2\344\270\276&\345\247\224\346\211\230.md" | 4 +- ...47\346\211\277\351\227\256\351\242\230.md" | 4 +- ...5\345\260\204&\346\211\251\345\261\225.md" | 4 +- .../8.Kotlin_\345\215\217\347\250\213.md" | 2 + KotlinCourse/9.Kotlin_androidktx.md | 12 +----- README.md | 40 +++++++++---------- 11 files changed, 52 insertions(+), 41 deletions(-) diff --git "a/KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" "b/KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" index 995523d5..526cfff2 100644 --- "a/KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" +++ "b/KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" @@ -1087,7 +1087,10 @@ class Person( -[下一篇:Kotlin学习教程(二)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%8C).md) + + + +- [下一篇:2.Kotlin_高阶函数&Lambda&内联函数](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/2.Kotlin_%E9%AB%98%E9%98%B6%E5%87%BD%E6%95%B0%26Lambda%26%E5%86%85%E8%81%94%E5%87%BD%E6%95%B0.md) --- diff --git "a/KotlinCourse/10.Kotlin_\350\256\276\350\256\241\346\250\241\345\274\217.md" "b/KotlinCourse/10.Kotlin_\350\256\276\350\256\241\346\250\241\345\274\217.md" index fc6a644e..e7afcfdc 100644 --- "a/KotlinCourse/10.Kotlin_\350\256\276\350\256\241\346\250\241\345\274\217.md" +++ "b/KotlinCourse/10.Kotlin_\350\256\276\350\256\241\346\250\241\345\274\217.md" @@ -470,9 +470,17 @@ The latest stock price has fell to 98 +- [上一篇:9.Kotlin_androidktx](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/9.Kotlin_androidktx.md) +参考 +=== + +- [Kotlin for Android Developers](https://leanpub.com/kotlin-for-android-developers) +- [Resources to Learn Kotlin](https://developer.android.com/kotlin/resources.html) +- [Kotlin语言中文站](https://www.kotlincn.net/docs/reference/coding-conventions.html) + --- diff --git "a/KotlinCourse/2.Kotlin_\351\253\230\351\230\266\345\207\275\346\225\260&Lambda&\345\206\205\350\201\224\345\207\275\346\225\260.md" "b/KotlinCourse/2.Kotlin_\351\253\230\351\230\266\345\207\275\346\225\260&Lambda&\345\206\205\350\201\224\345\207\275\346\225\260.md" index 77f1b45e..8be83bb9 100644 --- "a/KotlinCourse/2.Kotlin_\351\253\230\351\230\266\345\207\275\346\225\260&Lambda&\345\206\205\350\201\224\345\207\275\346\225\260.md" +++ "b/KotlinCourse/2.Kotlin_\351\253\230\351\230\266\345\207\275\346\225\260&Lambda&\345\206\205\350\201\224\345\207\275\346\225\260.md" @@ -729,8 +729,8 @@ startActivity() -[上一篇:Kotlin学习教程(六)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AD).md) -[下一篇:Kotlin学习教程(八)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AB).md) +- [上一篇:1.Kotlin_简介&变量&类&接口](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/1.Kotlin_%E7%AE%80%E4%BB%8B%26%E5%8F%98%E9%87%8F%26%E7%B1%BB%26%E6%8E%A5%E5%8F%A3.md) +- [下一篇:3.Kotlin_数字&字符串&数组&集合](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/3.Kotlin_%E6%95%B0%E5%AD%97%26%E5%AD%97%E7%AC%A6%E4%B8%B2%26%E6%95%B0%E7%BB%84%26%E9%9B%86%E5%90%88.md) --- diff --git "a/KotlinCourse/3.Kotlin_\346\225\260\345\255\227&\345\255\227\347\254\246\344\270\262&\346\225\260\347\273\204&\351\233\206\345\220\210.md" "b/KotlinCourse/3.Kotlin_\346\225\260\345\255\227&\345\255\227\347\254\246\344\270\262&\346\225\260\347\273\204&\351\233\206\345\220\210.md" index 3cab6d55..f7eb5bf6 100644 --- "a/KotlinCourse/3.Kotlin_\346\225\260\345\255\227&\345\255\227\347\254\246\344\270\262&\346\225\260\347\273\204&\351\233\206\345\220\210.md" +++ "b/KotlinCourse/3.Kotlin_\346\225\260\345\255\227&\345\255\227\347\254\246\344\270\262&\346\225\260\347\273\204&\351\233\206\345\220\210.md" @@ -561,8 +561,8 @@ loop@ for (i in 1..100) { ``` -[上一篇:Kotlin学习教程(二)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%8C).md) -[下一篇:Kotlin学习教程(四)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%9B%9B).md) +- [上一篇:2.Kotlin_高阶函数&Lambda&内联函数](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/2.Kotlin_%E9%AB%98%E9%98%B6%E5%87%BD%E6%95%B0%26Lambda%26%E5%86%85%E8%81%94%E5%87%BD%E6%95%B0.md) +- [下一篇:4.Kotlin_表达式&关键字](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/4.Kotlin_%E8%A1%A8%E8%BE%BE%E5%BC%8F%26%E5%85%B3%E9%94%AE%E5%AD%97.md) --- diff --git "a/KotlinCourse/4.Kotlin_\350\241\250\350\276\276\345\274\217&\345\205\263\351\224\256\345\255\227.md" "b/KotlinCourse/4.Kotlin_\350\241\250\350\276\276\345\274\217&\345\205\263\351\224\256\345\255\227.md" index f2196189..7c3921ea 100644 --- "a/KotlinCourse/4.Kotlin_\350\241\250\350\276\276\345\274\217&\345\205\263\351\224\256\345\255\227.md" +++ "b/KotlinCourse/4.Kotlin_\350\241\250\350\276\276\345\274\217&\345\205\263\351\224\256\345\255\227.md" @@ -218,6 +218,12 @@ for(num in nums) { - `object`对象声明并且它总是在`object{: .keyword }`关键字后跟一个名称。对象表达式:在要创建一个继承自某个(或某些)类型的匿名类的对象会 用到 + + + +- [上一篇:3.Kotlin_数字&字符串&数组&集合](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/3.Kotlin_%E6%95%B0%E5%AD%97%26%E5%AD%97%E7%AC%A6%E4%B8%B2%26%E6%95%B0%E7%BB%84%26%E9%9B%86%E5%90%88.md) +- [下一篇:5.Kotlin_内部类&密封类&枚举&委托](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/5.Kotlin_%E5%86%85%E9%83%A8%E7%B1%BB%26%E5%AF%86%E5%B0%81%E7%B1%BB%26%E6%9E%9A%E4%B8%BE%26%E5%A7%94%E6%89%98.md) + --- - 邮箱 :charon.chui@gmail.com diff --git "a/KotlinCourse/5.Kotlin_\345\206\205\351\203\250\347\261\273&\345\257\206\345\260\201\347\261\273&\346\236\232\344\270\276&\345\247\224\346\211\230.md" "b/KotlinCourse/5.Kotlin_\345\206\205\351\203\250\347\261\273&\345\257\206\345\260\201\347\261\273&\346\236\232\344\270\276&\345\247\224\346\211\230.md" index d6ea8ce6..0f50c432 100644 --- "a/KotlinCourse/5.Kotlin_\345\206\205\351\203\250\347\261\273&\345\257\206\345\260\201\347\261\273&\346\236\232\344\270\276&\345\247\224\346\211\230.md" +++ "b/KotlinCourse/5.Kotlin_\345\206\205\351\203\250\347\261\273&\345\257\206\345\260\201\347\261\273&\346\236\232\344\270\276&\345\247\224\346\211\230.md" @@ -670,8 +670,8 @@ class MutableUser(val map: MutableMap) { ``` -[上一篇:Kotlin学习教程(四)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%9B%9B).md) -[下一篇:Kotlin学习教程(六)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AD).md) +- [上一篇:4.Kotlin_表达式&关键字](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/4.Kotlin_%E8%A1%A8%E8%BE%BE%E5%BC%8F%26%E5%85%B3%E9%94%AE%E5%AD%97.md) +- [下一篇:6.Kotlin_多继承问题](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/6.Kotlin_%E5%A4%9A%E7%BB%A7%E6%89%BF%E9%97%AE%E9%A2%98.md) --- diff --git "a/KotlinCourse/6.Kotlin_\345\244\232\347\273\247\346\211\277\351\227\256\351\242\230.md" "b/KotlinCourse/6.Kotlin_\345\244\232\347\273\247\346\211\277\351\227\256\351\242\230.md" index 29bd7403..3dbf5fe7 100644 --- "a/KotlinCourse/6.Kotlin_\345\244\232\347\273\247\346\211\277\351\227\256\351\242\230.md" +++ "b/KotlinCourse/6.Kotlin_\345\244\232\347\273\247\346\211\277\351\227\256\351\242\230.md" @@ -191,8 +191,8 @@ fun main(args: Array) { -[上一篇:Kotlin学习教程(七)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%83).md) -[下一篇:Kotlin学习教程(八)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AB).md) +- [上一篇:5.Kotlin_内部类&密封类&枚举&委托](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/5.Kotlin_%E5%86%85%E9%83%A8%E7%B1%BB%26%E5%AF%86%E5%B0%81%E7%B1%BB%26%E6%9E%9A%E4%B8%BE%26%E5%A7%94%E6%89%98.md) +- [下一篇:7.Kotlin_注解&反射&扩展](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/7.Kotlin_%E6%B3%A8%E8%A7%A3%26%E5%8F%8D%E5%B0%84%26%E6%89%A9%E5%B1%95.md) --- diff --git "a/KotlinCourse/7.Kotlin_\346\263\250\350\247\243&\345\217\215\345\260\204&\346\211\251\345\261\225.md" "b/KotlinCourse/7.Kotlin_\346\263\250\350\247\243&\345\217\215\345\260\204&\346\211\251\345\261\225.md" index 42330826..8bd40753 100644 --- "a/KotlinCourse/7.Kotlin_\346\263\250\350\247\243&\345\217\215\345\260\204&\346\211\251\345\261\225.md" +++ "b/KotlinCourse/7.Kotlin_\346\263\250\350\247\243&\345\217\215\345\260\204&\346\211\251\345\261\225.md" @@ -1067,8 +1067,8 @@ class Group(val name: String) { -[上一篇:Kotlin学习教程(五)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%94).md) -[下一篇:Kotlin学习教程(七)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%83).md) +- [上一篇:6.Kotlin_多继承问题](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/6.Kotlin_%E5%A4%9A%E7%BB%A7%E6%89%BF%E9%97%AE%E9%A2%98.md) +- [下一篇:8.Kotlin_协程](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/8.Kotlin_%E5%8D%8F%E7%A8%8B.md) --- diff --git "a/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" "b/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" index 4826886f..04243795 100644 --- "a/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" +++ "b/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" @@ -769,6 +769,8 @@ println("Done!") ``` +- [上一篇:7.Kotlin_注解&反射&扩展](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/7.Kotlin_%E6%B3%A8%E8%A7%A3%26%E5%8F%8D%E5%B0%84%26%E6%89%A9%E5%B1%95.md) +- [下一篇:9.Kotlin_androidktx](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/9.Kotlin_androidktx.md) --- diff --git a/KotlinCourse/9.Kotlin_androidktx.md b/KotlinCourse/9.Kotlin_androidktx.md index aaef520a..5316334e 100644 --- a/KotlinCourse/9.Kotlin_androidktx.md +++ b/KotlinCourse/9.Kotlin_androidktx.md @@ -96,19 +96,11 @@ public class ForecastRequest(val zipCode: String) { -[上一篇:Kotlin学习教程(八)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AB).md) -[下一篇:Kotlin学习教程](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%8D%81).md) +- [上一篇:8.Kotlin_协程](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/8.Kotlin_%E5%8D%8F%E7%A8%8B.md) +- [下一篇:10.Kotlin_设计模式](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/10.Kotlin_%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md) -参考 -=== - -- [Kotlin for Android Developers](https://leanpub.com/kotlin-for-android-developers) -- [Resources to Learn Kotlin](https://developer.android.com/kotlin/resources.html) -- [Kotlin语言中文站](https://www.kotlincn.net/docs/reference/coding-conventions.html) - - --- - 邮箱 :charon.chui@gmail.com diff --git a/README.md b/README.md index 1a456b7f..78f801b2 100644 --- a/README.md +++ b/README.md @@ -144,16 +144,16 @@ Android学习笔记 - [Icon制作][223] - [Kotlin学习][48] - - [Kotlin学习教程(一)][180] - - [Kotlin学习教程(二)][181] - - [Kotlin学习教程(三)][182] - - [Kotlin学习教程(四)][183] - - [Kotlin学习教程(五)][184] - - [Kotlin学习教程(六)][185] - - [Kotlin学习教程(七)][186] - - [Kotlin学习教程(八)][187] - - [Kotlin学习教程(九)][188] - - [Kotlin学习教程(十)][197] + - [1.Kotlin_简介&变量&类&接口][180] + - [2.Kotlin_高阶函数&Lambda&内联函数][181] + - [3.Kotlin_数字&字符串&数组&集合][182] + - [4.Kotlin_表达式&关键字][183] + - [5.Kotlin_内部类&密封类&枚举&委托][184] + - [6.Kotlin_多继承问题][185] + - [7.Kotlin_注解&反射&扩展][186] + - [8.Kotlin_协程][187] + - [9.Kotlin_androidktx][188] + - [10.Kotlin_设计模式][197] @@ -492,15 +492,15 @@ Android学习笔记 [179]: https://github.com/CharonChui/AndroidNote/blob/master/BasicKnowledge/XmlPullParser.md "XmlPullParser" -[180]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%80).md "Kotlin学习教程(一)" -[181]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%8C).md "Kotlin学习教程(二)" -[182]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%89).md "Kotlin学习教程(三)" -[183]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%9B%9B).md "Kotlin学习教程(四)" -[184]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%94).md "Kotlin学习教程(五)" -[185]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AD).md "Kotlin学习教程(六)" -[186]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%83).md "Kotlin学习教程(七)" -[187]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AB).md "Kotlin学习教程(八)" -[188]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B9%9D).md "Kotlin学习教程(九)" +[180]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/1.Kotlin_%E7%AE%80%E4%BB%8B%26%E5%8F%98%E9%87%8F%26%E7%B1%BB%26%E6%8E%A5%E5%8F%A3.md "1.Kotlin_简介&变量&类&接口" +[181]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/2.Kotlin_%E9%AB%98%E9%98%B6%E5%87%BD%E6%95%B0%26Lambda%26%E5%86%85%E8%81%94%E5%87%BD%E6%95%B0.md "2.Kotlin_高阶函数&Lambda&内联函数.md" +[182]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/3.Kotlin_%E6%95%B0%E5%AD%97%26%E5%AD%97%E7%AC%A6%E4%B8%B2%26%E6%95%B0%E7%BB%84%26%E9%9B%86%E5%90%88.md "3.Kotlin_数字&字符串&数组&集合" +[183]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/4.Kotlin_%E8%A1%A8%E8%BE%BE%E5%BC%8F%26%E5%85%B3%E9%94%AE%E5%AD%97.md "4.Kotlin_表达式&关键字" +[184]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/5.Kotlin_%E5%86%85%E9%83%A8%E7%B1%BB%26%E5%AF%86%E5%B0%81%E7%B1%BB%26%E6%9E%9A%E4%B8%BE%26%E5%A7%94%E6%89%98.md "5.Kotlin_内部类&密封类&枚举&委托" +[185]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/6.Kotlin_%E5%A4%9A%E7%BB%A7%E6%89%BF%E9%97%AE%E9%A2%98.md "6.Kotlin_多继承问题" +[186]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/7.Kotlin_%E6%B3%A8%E8%A7%A3%26%E5%8F%8D%E5%B0%84%26%E6%89%A9%E5%B1%95.md "7.Kotlin_注解&反射&扩展" +[187]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/8.Kotlin_%E5%8D%8F%E7%A8%8B.md "8.Kotlin_协程" +[188]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/9.Kotlin_androidktx.md "9.Kotlin_androidktx" [189]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/%E5%85%AB%E7%A7%8D%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95.md "八种排序算法" [190]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/%E7%BA%BF%E7%A8%8B%E6%B1%A0%E7%AE%80%E4%BB%8B.md "线程池简介" [191]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md "设计模式" @@ -509,7 +509,7 @@ Android学习笔记 [194]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/ConstraintLaayout%E7%AE%80%E4%BB%8B.md "ConstraintLaayout简介" [195]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/Http%E4%B8%8EHttps%E7%9A%84%E5%8C%BA%E5%88%AB.md "Http与Https的区别" [196]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/Top-K%E9%97%AE%E9%A2%98.md "Top-K问题" -[197]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%8D%81).md "Kotlin学习教程(十)" +[197]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/10.Kotlin_%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md "10.Kotlin_设计模式" [198]: https://github.com/CharonChui/AndroidNote/blob/master/AppPublish/%E4%BD%BF%E7%94%A8Jenkins%E5%AE%9E%E7%8E%B0%E8%87%AA%E5%8A%A8%E5%8C%96%E6%89%93%E5%8C%85.md "使用Jenkins实现自动化打包" [199]: https://github.com/CharonChui/AndroidNote/tree/master/Dagger2 "Dagger2" [200]: https://github.com/CharonChui/AndroidNote/blob/master/Dagger2/1.Dagger2%E7%AE%80%E4%BB%8B(%E4%B8%80).md "1.Dagger2简介(一).md" From 096b5da5a9def2ac9e7953213e6f6f7f71ba9f3d Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 9 Apr 2021 15:54:50 +0800 Subject: [PATCH 052/213] update kotlin notes --- ...&\347\261\273&\346\216\245\345\217\243.md" | 134 ++++++++++++++++-- ...05\350\201\224\345\207\275\346\225\260.md" | 72 ++++++++++ ...0\347\273\204&\351\233\206\345\220\210.md" | 126 ++++++++++++++-- ...2\344\270\276&\345\247\224\346\211\230.md" | 58 +++++++- .../8.Kotlin_\345\215\217\347\250\213.md" | 19 +++ 5 files changed, 385 insertions(+), 24 deletions(-) diff --git "a/KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" "b/KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" index 526cfff2..c4e3454b 100644 --- "a/KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" +++ "b/KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" @@ -204,6 +204,20 @@ var weight = 70.5 // double } ``` + + +### 变量保存了指向对象的引用 + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/variable.jpg?raw=true) + +当该对象被赋值给变量时,这个对象本身并不会被直接赋值给当前的变量。相反,该对象的引用会被赋值给该变量。因为当前的变量存储的是对象的引用,因此它可以访问该对象。 + +如果你使用val来声明一个变量,那么该变量所存储的对象的引用将不可修改。然而如果你使用var声明了一个变量,你可以对该变量重新赋值。例如,如果我们使用代码: `x = 6`,将x的值赋为6,此时会创建一个值为6的新Int对象,并且x会存放该对象的引用。下面新的引用会替代原有的引用值被存放在x中: + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/var_chage.jpg?raw=true) + +**注意: 在Java中,数字类型是原生类型,所以变量存储的是实际数值。但是在Kotlin中的数字也是对象,而变量仅仅存储该数字对象的引用,并非对象本身。** + ### 优先使用val来避免副作用 在很多Kotlin的学习资料中,都会传递一个原则:优先使用val来声明变量。这相当正确,但更好的理解可以是:尽可能采用val、不可变对象及纯函数来设计程序。关于纯函数的概念,其实就是没有副作用的函数,具备引用透明性。 @@ -267,7 +281,7 @@ var size: Int = 2 这个例子中就会内存溢出。 `kotlin`为此提供了一种我们要说的后端变量,也就是`field`。编译器会检查函数体,如果使用到了它,就会生成一个后端变量,否则就不会生成。 -我们在使用的时候,用`field`代替属性本身进行操作。按照惯例`set`参数的名称是`value`,但是如果你喜欢你可以选择一个不同的名称。 +我们在使用的时候,用`field`代替属性本身进行操作。按照惯例`set`参数的名称是`value`,但是如果你喜欢你可以选择一个不同的名称。setter通过field标识更新变量属性值。field指的是属性的支持字段,你可以将其视为对属性的底层值的引用。在getter和setter中使用field代替属性名称很重要,因为这样可以阻止你陷入无限循环中。 ```kotlin class A { @@ -289,7 +303,17 @@ fun main() { 1 ``` +如果我们不手动写getter和setter方法,编译器会在编译代码时添加以下代码段: + +```kotlin +var myProperty: String + get() = field + set(value) { + field = value + } +``` +这意味着无论何时当你使用点操作符来获取或设置属性值时,实际上你总是调用了属性的getter或是setter。那么,为什么编译器要这么做呢?为属性添加getter和setter意味着有访问该属性的标准方法。getter处理获取值的所有请求,而setter处理所有属性值设置的请求。因此,如果你想要改变处理这些请求的方式,你可以在不破坏任何人代码的前提下进行。通过将其包装在getter和setter中来输出对属性的直接访问称为数据隐藏。 ## 延迟初始化 @@ -375,7 +399,20 @@ val sex: String by lazy(LazyThreadSafetyMode.NONE) { ## 类的定义:使用`class`关键字 +当你在定义类的时候,你需要想想该类所创建的对象需要什么。你需要考虑: + +- 每个对象自身的特点 + + 对象自身的特点称为属性(properties)。它们代表了对象自身的状态(数据),并且该类中的每一个对象都有自己独特的数值。例如,一个狗(Dog)类可能有名字(name)、体重(weight)和品种(breed)属性。一个歌曲(Song)类可能有标题(title)和演唱者(artist)属性。 + +- 每个对象的行为 + + 对象的行为是它们的函数(functions)。它们决定了对象的行为,并且可能回使用对象的属性。例如,之前提到的Dog类,可能具有吠叫(bark)函数;Song这个类可能会有播放(play)函数。 + + + 类可以包含: + - 构造函数和初始化块 - 函数 - 属性 @@ -383,29 +420,67 @@ val sex: String by lazy(LazyThreadSafetyMode.NONE) { - 对象声明 -```kotlin -class MainActivity{ +你可以将类想象成一个对象的模板,因为它告诉编译器如何创建该特定类的对象。它还将告诉编译器每个对象应该具有哪些属性,并且从该类生成的每个对象都可以拥有自己独有的属性值。例如,每个Dog对象都有自己的名称、重量和品种属性,每个Dog的属性值都可以是不同的。 + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/kotlin_class_1.jpg?raw=true) + + + + +```kotlin +class Dog(val name: String, var weight: Int, val breed: String){ + fun bark() { + + } } ``` 如果有参数的话你只需要在类名后面写上它的参数,如果这个类没有任何内容可以省略大括号: ```kotlin -class Person(name: String, age: Int) +class Dog(val name: String, var weight: Int, val breed: String) ``` ### 创建类的实例 ```kotlin -val person = Person("charon", 18) +val myDog = Dog("Fido", 70, "Mixed" ) ``` 上面的类有一个默认的构造函数。 注意:创建类的实例不用`new`了啊。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/kotlin_class_dog_sample.jpg?raw=true) + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/kotlin_class_dog.jpg?raw=true) + +类中所定义的函数又称为成员函数。有时也被称为方法。 + +#### 创建对象的执行过程 + +```kotlin +var myDog = Dog("Fido", 70, "Mixed") +``` + +1. 系统会为每个传入Dog构造函数的参数创建一个对象。它会创建一个值为“Fido”的String,一个值为70的Int,以及一个值为“Mixed”的String。 +2. 系统会为一个新的Dog对象分配空间,并且Dog构造函数会被调用。 +3. Dog构造函数定义了三个属性:名称、重量以及品种。在这个现象背后,每一个属性实际上是一个变量。对于构造函数中定义的每个属性,都会有一个相应类型的变量被创建。 +4. 相应的变量的引用将会被赋值给Dog的属性。例如,值为“Fido”的String将会被赋值给name属性。 +5. 最后,这个新的Dog对象的引用将会被赋值给名为myDog的Dog变量。 + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/kotlin_dog_new.jpg?raw=true) + + + + + + + ### 构造函数 +构造函数包含了初始化对象所需的代码。它在对象被分配给引用之前运行,这意味着你有机会对对象进行一些内部操作以便其被使用。大多人使用构造函数来定义对象的属性,并且给这些属性赋值。每当你创建一个新的对象,该对象所属的类的构造函数将会被调用。构造函数在你初始化对象时被调用。它通常被用于定义对象的属性,并且对属性赋值。 + 在`Kotlin`中的一个类可以有一个主构造函数和一个或多个次构造函数。 #### 主构造函数 @@ -616,6 +691,8 @@ data class Artist( var mbid: String) ``` +数据类自动覆盖它们的equals方法以改变==操作符的行为,由此通过检查对象的每个属性值来判断是否相等。例如,假设你创建了两个属性值完全相同的Recipe对象,使用==操作符对它们进行比较将返回true,因为它们存放了相同的数据:除了提供从Any父类继承的equals方法的新实现,数据类还覆盖了hashCode和toString方法。 + 通过数据类,会自动提供以下函数: - 所有属性的`get() set()`方法 @@ -649,7 +726,13 @@ val charon2 = charon.copy(age = 19) - data class之前不能用abstract、open、sealed或者inner进行修饰 - 在Kotlin 1.1版本前数据类只允许实现接口,之后的版本既可以实现接口也可以继承类 -## 多声明 + + +与任何其他类一样,你可以向数据类添加属性和方法,只需要将它们包含在类主体中。但是有一个大问题。在编译器生成数据类的方法实现时,比如覆盖equals方法和创建copy方法,它仅包含在主构造函数中定义的属性。因此如果你在数据类主体中定义添加的属性,则它们不会被包含到任何编译器生成的方法中。 + +### 数据类定义了componentN方法 + +定义数据类时,编译器会自动向该类添加一组方法,你可以将其作为访问对象属性值的替代方法。它们被称为componentN方法,其中N表示被访问属性的编号(按声明排序)。 多声明,也可以理解为变量映射,这就是编译器自动生成的`componentN()`方法。 @@ -689,6 +772,10 @@ open class Person(num: Int) class SuperPerson(num: Int) : Person(num) ``` +冒号后面的Person(num)会调用Person类的构造函数,以确保所有的初始化代码(例如给属性赋值)能够被执行。调用父类构造函数是强制性的:如果父类有主构造函数,你必须在子类头中调用它,否则代码将无法通过编译。请记住,即使你没有在父类中显式地添加构造函数,编译器也会在编译代码的时候自动创建一个空构造函数。假如我们不想为Person类添加构造函数,因此编译器在编译代码的时候创建了一个空构造函数。该构造函数通过使用Person()被调用。 + + + 如果该类有一个主构造函数,其基类必须用基类型的主构造函数参数就地初始化。 如果类没有主构造函数,那么每个次构造函数必须使用`super`关键字初始化其基类型,或委托给另一个构造函数做到这一点。 注意,在这种情况下,不同的次构造函数可以调用基类型的不同的构造函数: @@ -849,7 +936,7 @@ open class SuperPerson(num: Int) : Person(num) { } ``` -每个声明的属性可以由具有初始化器的属性或者具有`get`方法的属性覆盖,你也可以用一个`var`属性覆盖一个`val`属性,但反之则不行。 +每个声明的属性可以由具有初始化器的属性或者具有`get`方法的属性覆盖,如果某个属性在父类中被定义为val,你可以在子类中使用var属性覆盖它。只需要覆盖该属性并将其声明为var即可。请注意,这只适用于这一种方式。如果尝试使用val覆盖var属性,编译器将会感到沮丧并拒绝编译你的代码。 @@ -886,9 +973,10 @@ abstract class Derived : Base() { - ## 接口:使用`interface`关键字 +接口可以让你在父类层次结构之外定义共同的行为接口用于为共同行为定义协议,使你可以不依赖严格的继承结构却又可以利用多态。与抽象类类似,接口不能被实例化且可以定义抽象或具体的方法和属性,但两者有一个关键的不同点:类可以实现多个接口,但是只能继承于一个直接父类。所以接口不仅拥有抽象类的优点,而且使用起来更加灵活。 + ```kotlin interface FlyingAnimal { fun fly() @@ -964,6 +1052,26 @@ toast("Hello") toast("Hello", Toast.LENGTH_LONG) ``` +### 无参主函数 + +如果你使用的是Kotlin1.2或更早的版本,若想正常运行程序,你的主函数必须写成如下形式: + +```kotlin +fun main(args: Array) { + // ... +} +``` + +从Kotlin1.3版本器,你可以忽略main函数的参数,写成如下形式: + +```kotlin +fun main() { + // ... +} +``` + + + ### 可变长参数函数:使用`vararg`关键字 @@ -981,6 +1089,16 @@ fun main(args: Array) { } ``` +如果你有一个现有的值数组,则可以通过在数组名前加上`*`来将这些值传递给该函数。星号`(*)`被称为扩展运算符,以下是它的一些使用示例: + +```kotlin +vval myArray = arrayOf(1, 2, 3, 4, 5) +val mList = vars(*myArray) +val mList2 = vars(0, *myArray, 6, 7) +``` + + + ### `Unit`:让函数调用皆为表达式 diff --git "a/KotlinCourse/2.Kotlin_\351\253\230\351\230\266\345\207\275\346\225\260&Lambda&\345\206\205\350\201\224\345\207\275\346\225\260.md" "b/KotlinCourse/2.Kotlin_\351\253\230\351\230\266\345\207\275\346\225\260&Lambda&\345\206\205\350\201\224\345\207\275\346\225\260.md" index 8be83bb9..78f38efb 100644 --- "a/KotlinCourse/2.Kotlin_\351\253\230\351\230\266\345\207\275\346\225\260&Lambda&\345\206\205\350\201\224\345\207\275\346\225\260.md" +++ "b/KotlinCourse/2.Kotlin_\351\253\230\351\230\266\345\207\275\346\225\260&Lambda&\345\206\205\350\201\224\345\207\275\346\225\260.md" @@ -252,6 +252,8 @@ val foo = { x: Int -> > “Lambda 表达式”(lambda expression)其实就是匿名函数,`Lambda`表达式基于数学中的`λ`演算得名,直接对应于其中的`lambda`抽象 > `(lambda abstraction)`,是一个匿名函数,即没有函数名的函数。`Lambda`表达式可以表示闭包(注意和数学传统意义上的不同)。 + + `Java 8`的一个大亮点是引入`Lambda`表达式,使用它设计的代码会更加简洁。 ```java @@ -327,6 +329,50 @@ val sum: (Int, Int) -> Int = { x, y -> x + y } 则此`Lambda`表达式变成`val sum = { x: Int, y: Int -> x + y }`,此时`Lambda`表达式会根据主体中的最后一个(或可能是单个)表达式会视为 返回值。当然,在某些特定情况下,`x`、`y`的类型了是可以推断的,所以`val sum = { x, y -> x + y }`。 + + +通过调用lambda来执行它的代码你可以使用invoke函数调用lambda,并传入参数的值。例如,以下代码定义了变量addInts,并将用于将两个Int参数相加的lambda赋值给它。然后代码调用了该lambda,传入参数值6和7,并将结果赋值给变量result: + +```kotlin +val addInts = { x: Int, y: Int -> x + y } +val result = addInts.invoke(6, 7) +// 还可以使用如下快捷方式调用lambda: +val result = addInts(6, 7) + +``` + +### lambda表达式类型 + +就像任何其他类型的对象一样,lambda也具有类型。然而,lambda类型的不同点在于,它不会为lambda的实现指定类名,而是指定lambda的参数和返回值的类型。lambda类型的格式如下: + +```kotlin +(parameters) -> return_type +``` + +因此,如果你的lambda具有单独的Int参数并返回一个字符串,如下代码所示: + +```kotlin +val msg = { x: Int -> "xxx" } +``` + +其类型为: + +```kotlin +(Int) -> String +``` + +如果将lambda赋值给一个变量,编译器会根据该lambda来推测变量的类型,如上例所示。然而,就像任何其他类型的对象一样,你可以显式地定义该变量的类型。例如,以下代码定义了一个变量add,该变量可以保存对具有两个Int参数并返回Int类型的lambda的引用: + +```kotlin +val add: (Int, Int) -> Int + +add = { x: Int, y: Int -> x + y } +``` + +Lambda类型也被认为是函数类型。 + + + ## Lambda开销 ```kotlin @@ -344,6 +390,32 @@ listOf(1, 2, 3).forEach { item -> foo(item) } 默认情况下,我们可以直接用it来代表item,而不需要用item -> 进行声明。 +你可以将单独的参数替换为it。 + +如果lambda具有一个单独的参数,而且编译器能够推断其类型,你可以省略该参数,并在lambda的主体中使用关键字it指代它。 + +要了解它是如何工作的,如前所述,假设使用以下代码将lambda赋值给变量: + +```kotlin +val addFive: (Int) -> Int = { x -> x + 5 } +``` + +由于lambda具有单独的参数x,而且编译器能够推断出x为Int类型,因此我们可以省略该x参数,并在lambda的主体中使用it替换它: + +```kotlin +val addFive: (Int) - Int = { it + 5 } +``` + +在上述代码中,{it+5}等价于{x->x+5},但更加简洁。请注意,你只能在编译器能够推断该参数类型的情况下使用it语法。例如,以下代码将无法编译,因为编译器不知道it应该是什么类型: + +```kotlin +val addFive = { it + 5 } // 该代码无法编译,因为编译器不能推断其类型 +``` + + + + + 我们看一下foo函数用IDE转换后的Java代码: ```java diff --git "a/KotlinCourse/3.Kotlin_\346\225\260\345\255\227&\345\255\227\347\254\246\344\270\262&\346\225\260\347\273\204&\351\233\206\345\220\210.md" "b/KotlinCourse/3.Kotlin_\346\225\260\345\255\227&\345\255\227\347\254\246\344\270\262&\346\225\260\347\273\204&\351\233\206\345\220\210.md" index f7eb5bf6..2a31cef7 100644 --- "a/KotlinCourse/3.Kotlin_\346\225\260\345\255\227&\345\255\227\347\254\246\344\270\262&\346\225\260\347\273\204&\351\233\206\345\220\210.md" +++ "b/KotlinCourse/3.Kotlin_\346\225\260\345\255\227&\345\255\227\347\254\246\344\270\262&\346\225\260\347\273\204&\351\233\206\345\220\210.md" @@ -27,6 +27,7 @@ Byte 8 注意在`Kotlin`中字符不是数字,字符用`Char`类型表示。它们不能直接当作数字 + ### 字面常量 数值常量字面值有以下几种: @@ -87,6 +88,40 @@ toDouble(): Double toChar(): Char ``` +#### 数值类型转换背后发生了什么 + +```kotlin +var x = 5 // 这行代码创建了一个Int类型的变量x以及一个Int类型值为5的对象。x保存了该对象的引用 +var z : Long = x.toLong() // 这行代码创建了一个新的Long变量z。x对象的toLong()函数被调用并且创建了一个值为5的Long对象,该Long对象的引用被存储在z中 +``` + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/var_type_change.jpg?raw=true) + +该方法可以较好地应用于从存储小数据的类型转换为能存储较大数据的类型。那么,如果该数值超出了新对象所能存储的范围该怎么办? + +试图将一个大的数值放入一个容量较小的变量中就好比试图将桶装咖啡倒入小茶杯中。有些咖啡会被倒入茶杯中,但是有些会溢出。 + +假如你想将Long的值放入Int中。正如我们之前所提到的,Long可以容纳比Int更大的数字。 + +因此如果Long的值在Int可存储的范围之内,那么从Long转换为Int是没有问题的。例如,将一个值为42的Long转换为Int将得到一个值为42的Int: + +```kotlin +var x = 42L +var y: Int = x.toInt() // 值为42 +``` + +但是如果Long的值超出了Int能容纳的范围,那么编译器将会舍弃超出的部分,此时你会得到一个奇怪(仍可计算)的数值。例如: + +```kotlin +var x = 1234567890123 +var y: Int = x.toInt() +println(y) // 1912276171 +``` + +这设计数值正负、位运算、二进制等其他一些计算机知识。这里不再细说。 + + + ### 运算 这是完整的位运算列表(只用于`Int`和`Long`): @@ -231,12 +266,23 @@ print(boxedA == anotherBoxedA) // 输出“true” ### 数组 +你可以将数组想象成一托盘的杯子,其中每个杯子都是一个变量。 + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/array.jpg?raw=true) + + + 数组用类`Array`实现,并且还有一个`size`属性及`get`和`set`方法,由于使用`[]`重载了`get`和`set`方法,所以我们可以通过下标很方便的获取或者 设置数组对应位置的值。 `Kotlin`标准库提供了`arrayOf()`创建数组和`xxArrayOf`创建特定类型数组 ```kotlin -val array = arrayOf(1, 2, 3) +val myArray = arrayOf(1, 2, 3) +``` + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/array_myarray.jpg?raw=true) + +```kotlin val countries = arrayOf("UK", "Germany", "Italy") val numbers = intArrayOf(10, 20, 30) val array1 = Array(10, { k -> k * k }) @@ -250,6 +296,30 @@ studentArray[0] = Student("james") ### 集合 +Kotlin有三个主要的集合类型(List、Set和Map),每一个都有不同的用途。 + +- List——当顺序很重要 + + List知道而且在意索引的位置。它知道List中的元素在哪里,而且你可以使多个元素指向同一个对象。 + +- Set——当唯一性很重要 + + Set不允许重复,而且不在意值的存放顺序。你不可以使多个元素指向同一个对象,或是被认为相等的两个对象。 + + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/kotlin_list_set.jpg?raw=true) + +- Map——当键检索很重要 + + Map使用键值对,它知道与给定键相关联的值。你可以使两个键指向同一个对象,但不可以有重复的键。键通常为String类型(因此你可以创建例如键值对属性列表),但它也可以是任意对象。 + + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/kotlin_map.jpg?raw=true) + + + +简单的List、Set和Map是不可变的,这意味着集合被初始化后不能再添加或移除元素。如果想要添加或移除元素,Kotlin提供了可变的子类型作为替代方案:MutableList、MutableSet和MutableMap。因此,如果想要利用List的所有优势,并希望能够更新其内容,请使用MutableList。 + + + `Kotlin`的`List`类型是一个提供只读操作如`size`、`get`等的接口。和`Java`类似,它继承自`Collection`进而继承自`Iterable`。 改变`list`的方法是由`MutableList`加入的。这一模式同样适用于`Set/MutableSet`及`Map/MutableMap`。 @@ -503,16 +573,6 @@ val l = b!!.length 因此,如果你想要一个NPE,你可以得到它,但是你必须显式要求它,否则它不会不期而至。 - -#### 安全的类型转换 - -如果对象不是目标类型,那么常规类型转换可能会导致`ClassCastException`。 -另一个选择是使用安全的类型转换,如果尝试转换不成功则返回`null{: .keyword }`: - -```kotlin -val aInt: Int? = a as? Int -``` - #### 可空类型的集合 如果你有一个可空类型元素的集合,并且想要过滤非空元素,你可以使用`filterNotNull`来实现。 @@ -526,7 +586,7 @@ val intList: List = nullableList.filterNotNull() ### 使用类型检测及自动类型转换 -`is`运算符检测一个表达式是否某类型的一个实例。 如果一个不可变的局部变量或属性已经判断出为某类型,那么检测后的分支中可以直接当作该类型使用, +`is`运算符检测一个表达式是否某类型的一个实例。 如果一个不可变的局部变量或属性已经判断出为某类型,那么检测后的分支中可以直接当作该类型使用,在大多数情况下,is操作符会进行智能转换。转换表示编译器将变量当作与其声明的类型不同的类型,而智能转换是说编译器替你自动地进行转换。 无需显式转换: ```kotlin @@ -540,6 +600,48 @@ fun getStringLength(obj: Any): Int? { } ``` + + +只要编译器能够保证在介于判断对象类型和被使用之间不能修改变量,is操作符就会进行智能转换。 + +例如,在上面的代码中,编译器知道在介于调用is操作符和调用String的某个方法之间,item变量不能被赋予另一类型的引用。但是在一些特殊情况下,智能转换不会生效。例如,is操作符不会对类中的var属性进行智能转换,那是因为编译器无法保证别的代码不会溜进来更新该属性。这意味着如下代码将不能编译,因为编译器不能将r变量智能转换为一个Wolf对象: + +```kotlin +class MyRomable { + var r: Roamable = Wolf() + + fun myFunction() { + if (r is Wolf) { + r.eat() // 编译器无法智能的将Roamable的r属性转换成一个Wolf对象,这事因为编译器不能保证在判断r属性类型和使用它的器件,其它代码不会更新该属性,因此这段代码不能编译成功。 + } + } +} +``` + +那么遇到这种情况我们应该如何处理呢?你无须记住所有不能使用智能转换的场景。如果你尝试使用智能转换的方式不合理,编译器会提醒你。编译器会提醒你。 + +#### 安全的类型转换 + +如果对象不是目标类型,那么常规类型转换可能会导致`ClassCastException`。 +另一个选择是使用安全的类型转换,如果尝试转换不成功则返回`null{: .keyword }`: + +```kotlin +val aInt: Int? = a as? Int +``` + +如果你想要访问某个潜在对象的行为,但编译器无法对其进行智能转换,你可以显式地将该对象转换成合适的类型。假设你能够确定名为r的Roamable类型变量保存的是Wolf对象的引用。在这种情况下,你可以使用as操作符去复制一份Roamable类型变量中保存的引用,并强制地将该引用赋给一个新的Wolf类型变量。然后你就可以使用该Wolf类型变量去访问Wolf的行为。具体代码如下: + +```kotlin +if (r is Wolf) { + var wolf = r as Wolf // 这段代码显式地将对象转换为Wolf类型,使你可以调用它的方法 + wolf.eat() +} +``` + + + + + ### 返回和跳转 `Kotlin`有三种结构化跳转表达式: diff --git "a/KotlinCourse/5.Kotlin_\345\206\205\351\203\250\347\261\273&\345\257\206\345\260\201\347\261\273&\346\236\232\344\270\276&\345\247\224\346\211\230.md" "b/KotlinCourse/5.Kotlin_\345\206\205\351\203\250\347\261\273&\345\257\206\345\260\201\347\261\273&\346\236\232\344\270\276&\345\247\224\346\211\230.md" index 0f50c432..1d18d2c4 100644 --- "a/KotlinCourse/5.Kotlin_\345\206\205\351\203\250\347\261\273&\345\257\206\345\260\201\347\261\273&\346\236\232\344\270\276&\345\247\224\346\211\230.md" +++ "b/KotlinCourse/5.Kotlin_\345\206\205\351\203\250\347\261\273&\345\257\206\345\260\201\347\261\273&\346\236\232\344\270\276&\345\247\224\346\211\230.md" @@ -171,9 +171,60 @@ val searchName: String = Icon.SEARCH.name() val searchPosition: Int = Icon.SEARCH.ordinal() ``` -### 密封类 -Kotlin除了可以利用final来限制类的继承之外,还可以通过密封类的语法来限制一个类的继承,比如: + +枚举可以让你创建受限制的一组值,但在某些情况下,你需要更多的灵活性。假设你希望能在应用程序中使用两种不同的消息类型:一种用于”成功“,另一种用于”失败“,并且你希望能够将邮件限制为这两种类型。 + +如果你用枚举类对此进行建模,代码会如下: + +```kotlin +enum class MessageType(var msg: String) { + SUCCESS("Yay!"), + FAILURE("Boo!") +} +``` + +但是使用这种方法存在两个问题: + +- 每个值都是一个常量,并且仅作为单个实例存在。 + + 例如,你无法只在特定情况下修改SUCCESS的msg属性--因为一旦更改,代码中其他SUCCESS出现的地方也会被相应更改。 + +- 每个值必须有相同的属性和函数。 + + 向FAILURE值中添加Exception属性十分有用,它可以帮助你检查哪里出错了。但是枚举类不允许你这么做。 + +那么有其他的方法吗? 密封类可以拯救你。 + +### 密封类 + +Kotlin除了可以利用final来限制类的继承之外,还可以通过密封类的语法来限制一个类的继承。密封类就像枚举类的加强版本。它允许你将类层次结构限制为一组特定的子类型,每个子类型都可以定义自己的属性和函数。与枚举类不同,你可以创建每种类型的多个实例。 你可以通过使用sealed前缀类名来创建密封类。例如,以下代码创建一个名为MessageType的密封类,其中包含名为MessageSuccess和MessageFailure的子类型。每个子类型都有一个名为msg的String属性,并且MessageFailure子类型中有一个额外的名为e的Exception属性: + +```kotlin +sealed class MessageType +// MessageSuccess和MessageFailure从MessageType继承而来,并且它们自己的类型是在自己的构造函数中定义的 +class MessageSuccess(var msg: String) : MessageType() +class MessageFailure(var msg: String, var e: Exception) : MessageType() +``` + +由于MessageType是一个有限子类的密封类。你可以使用when来检查每个子类型,这样可以避免使用额外的else子句,如以下代码所示: + +```kotlin +fun main(args: Array) { + val messageSuccess = MessageSuccess("Yay!") + val messageSuccess2 = MessageSuccess("It worked!") + val messageFailure = MessageFailure("Boo!", Exception("Gone wrong.")) + + var myMessageType: MessageType = messageFailure + val myMessage = when(myMessageType) { + is MessageSuccess -> myMessageType.msg + is MessageFailure -> myMessageType.msg + myMessageType.e.message + } + println(myMessage) +} +``` + +Kotlin通过sealed关键字来修饰一个类为密封类,若要继承则需要将子类定义在同一个文件中,其他文件中的类将无法继承他。但这种方式也有它的局限性。即它不能被初始化,因为它背后是基于一个抽象类实现的。 ```kotlin sealed class Bird { @@ -182,8 +233,7 @@ sealed class Bird { } ``` -Kotlin通过sealed关键字来修饰一个类为密封类,若要继承则需要将子类定义在同一个文件中,其他文件中的类将无法继承他。 -但这种方式也有它的局限性。即它不能被初始化,因为它背后是基于一个抽象类实现的,这一点可以从它转换后的Java代码看出: +这一点可以从它转换后的Java代码看出: ```java public abstract class Bird { diff --git "a/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" "b/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" index 04243795..727998d8 100644 --- "a/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" +++ "b/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" @@ -13,6 +13,8 @@ Kotlin引入了协程(Coroutine)来支持更好的异步操作,利用它 +协程就像一个轻量级的线程在幕后,启动协程就像启动一个单独的执行线程。线程在其他语言中很常见,例如Java,协程和线程可以并行运行,并互相通信。然而,不同点在于使用协程比使用线程更加高效。在性能方面,启动一个线程并使其保持运行是非常昂贵的。处理器通常只能同时运行有限数量的线程,并且运行尽可能少的线程会更高效。而另一方面,协程默认运行在共享的线程池中,同一个线程可以运行多个协程。由于使用的线程较少,当你想要运行异步任务时,使用协程会更加高效。 + ## 使用 如需在 Android 项目中使用协程,请将以下依赖项添加到应用的build.gradle文件中: @@ -25,6 +27,8 @@ dependencies { ## 示例 +在代码中,我们使用GlobalScope.launch在后台运行一个新的协程。在幕后,它创建了一个新的线程使协程在其中运行, + ```kotlin import kotlinx.coroutines.* @@ -44,6 +48,21 @@ World! 本质上,协程是轻量级的线程。它们在某些CoroutineScope上下文中与launch协程构建器一起启动。 这里我们在ClobalScope中启动了一个新的协程,这意味着新协程的生命周期只受整个应用程序的生命周期限制。 + + +使用runBlocking在相同作用域内运行协程如果希望代码在相同线程、不同协程中运行,你可以使用runBlocking函数。runBlocking是一个高阶函数,它会阻塞当前线程,直到传入其中的代码运行完成。runBlocking函数定义了一个作用域,传入其中的代码继承该作用域。在本例中,我们可以使用该作用域在同一线程中运行不同的协程。 + + + +delay函数暂停当前协程在这种情况下,更好的解决方案是使用协程的delay函数取而代之。该函数与Thread.sleep有相似的效果,除了它会暂停当前协程而不是当前线程。它将协程挂起指定的时长,这允许运行该线程中的其他代码。delay函数可用于以下两种情况: + +- 在协程中使用。 +- 在某个编译器知道将会暂停或挂起的函数中使用,在函数中调用可挂起的函数时(例如delay),该函数必须被标记为suspend。 + +例如,以下代码将协程延迟1秒: + + + ```kotlin import kotlinx.coroutines.* From 79db2bf48778f9b04c4beed7405ce4a8b819e6cb Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 12 Apr 2021 14:39:23 +0800 Subject: [PATCH 053/213] add os notes --- ...MaterialDesign\344\275\277\347\224\250.md" | 281 +++++++++++++++++- .../Parcelable\345\217\212Serializable.md" | 2 + ...73\347\273\237\347\256\200\344\273\213.md" | 96 +++++- ...13\344\270\216\347\272\277\347\250\213.md" | 37 ++- ...05\345\255\230\347\256\241\347\220\206.md" | 40 ++- 5 files changed, 441 insertions(+), 15 deletions(-) diff --git "a/AdavancedPart/MaterialDesign\344\275\277\347\224\250.md" "b/AdavancedPart/MaterialDesign\344\275\277\347\224\250.md" index c6f3788d..549c18e9 100644 --- "a/AdavancedPart/MaterialDesign\344\275\277\347\224\250.md" +++ "b/AdavancedPart/MaterialDesign\344\275\277\347\224\250.md" @@ -26,8 +26,9 @@ Material Design Theme --- - 禁止Action Bar - 可以通过使用`Material theme`来让应用使用`Material Design`。想要使用`ToolBar`需要先禁用`ActionBar`。 - 可以通过自定义`theme`继承`Theme.AppCompat.Light.NoActionBar`或者在`theme`中通过以下配置来进行。 + ToolBar相当于是ActionBar的替代版,因此需要制定一个不带ActionBar的主题, + 可以通过自定义`theme`继承`Theme.AppCompat.Light.NoActionBar`或者在`theme`中通过以下配置来进行禁用`ActionBar`。 + ```xml false true @@ -59,8 +60,8 @@ Material Design Theme 配置的这几种颜色分别如下图所示: ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/material_color.png?raw=true) - 里面没有`colorAccent`的颜色,这个颜色是设置`Checkbox`等控件选中时的颜色。 - +里面没有`colorAccent`的颜色,唯独colorAccent这个属性比较难理解,它不是用来指定Checkbox`等某一个按钮的颜色,而是更多表达了一个强调的意思,比如一些控件的选中状态也会使用colorAccent的颜色。 + 在`values-v21`中的`style.xml`中同样自定义`AppTheme`主题: ```xml - ``` - - 配置的这几种颜色分别如下图所示: - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/material_color.png?raw=true) -里面没有`colorAccent`的颜色,唯独colorAccent这个属性比较难理解,它不是用来指定Checkbox`等某一个按钮的颜色,而是更多表达了一个强调的意思,比如一些控件的选中状态也会使用colorAccent的颜色。 - - 在`values-v21`中的`style.xml`中同样自定义`AppTheme`主题: - ```xml - - ``` - -- 在`Manifest`文件中设置`AppTheme`主题: - - ```xml - - - - - ``` - 这里说一下为什么要在`values-v21`中也自定义个主题,这是为了能让在`21`以上的版本能更好的使用`Material Design`, -在21以上的版本中会有更多的动画、特效等。 - -- 让Activity继承AppCompatActivity - - ```java - public class MainActivity extends AppCompatActivity { - ... - } - ``` - -- 在布局文件中进行声明 - - 声明`toolbar.xml`,我们把他单独放到一个文件中,方便多布局使用: - ```xml - - ``` - 在`Activity`的布局中使用`ToolBar`: - - ```xml - - - - - - - - - - ``` - -- 在Activity中设置ToolBar - ```java - public class MainActivity extends AppCompatActivity{ - private Context mContext; - private Toolbar mToolbar; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - mContext = this; - mToolbar = (Toolbar) findViewById(R.id.toolbar); - mToolbar.setTitle(R.string.app_name); - // 将ToolBar设置为ActionBar,这样一设置后他就能像ActionBar一样直接显示menu目录中的菜单资源 - // 如果不用该方法,那ToolBar就只是一个普通的View,对menu要用inflateMenu去加载布局。 - setSupportActionBar(mToolbar); - getSupportActionBar().setDisplayShowHomeEnabled(true); - } - } - ``` - -到这里运行项目就可以了,就可以看到一个简单的`ToolBar`实现。 - -接下来我们看一下`ToolBar`中具体有哪些内容: - -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/ToolBar_content.jpg?raw=true) - -我们可以通过对应的方法来修改他们的属性: -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/toolbarCode.png?raw=true) - -ToolBar最左侧的这个按钮就叫做HomeAsUp按钮,它默认的图标是一个返回的箭头,含义是返回上一个活动,可以通过setDisplayHomeAsUpEnabled来让导航按钮显示出来。 可以在onOptionsItemSelected()方法中对HomeAsUp安阿牛的点击事件进行处理,HomeAsUp按钮的id永远都是android.R.id.home。 - -​ - -对于`ToolBar`中的`Menu`部分我们可以通过一下方法来设置: -```java -toolbar.inflateMenu(R.menu.menu_main); -toolbar.setOnMenuItemClickListener(); -``` -或者也可以直接在`Activity`的`onCreateOptionsMenu`及`onOptionsItemSelected`来处理: -```java -@Override -public boolean onCreateOptionsMenu(Menu menu) { - // Inflate the menu; this adds items to the action bar if it is present. - getMenuInflater().inflate(R.menu.menu_main, menu); - return true; -} - -@Override -public boolean onOptionsItemSelected(MenuItem item) { - // Handle action bar item clicks here. The action bar will - // automatically handle clicks on the Home/Up button, so long - // as you specify a parent activity in AndroidManifest.xml. - int id = item.getItemId(); - - //noinspection SimplifiableIfStatement - if (id == R.id.action_settings) { - return true; - } - if (id == R.id.action_search) { - Toast.makeText(getApplicationContext(), "Search action is selected!", Toast.LENGTH_SHORT).show(); - return true; - } - return super.onOptionsItemSelected(item); -} -``` -`menu`的实现如下: -```xml - - - - - - - -``` - -如果想要对`NavigationIcon`添加点击实现: -```java -toolbar.setNavigationOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - onBackPressed(); - } -}); -``` - -运行后发现我们强大的`Activity`切换动画怎么在`5.0`一下系统上实现呢?`support v7`包也帮我们考虑到了。使用`ActivityOptionsCompat` -及`ActivityCompat.startActivity`,但是悲剧了,他对4.0一下基本都无效,而且就算在4.0上很多动画也不行,具体还是用其他 -大神在`github`写的开源项目吧。 - - -- 动态取色Palette - - `Palette`这个类中可以提取一下集中颜色:   - - - Vibrant (有活力) - - Vibrant dark(有活力 暗色) - - Vibrant light(有活力 亮色) - - Muted (柔和) - - Muted dark(柔和 暗色) - - Muted light(柔和 亮色) - - ```java - //目标bitmap,代码片段 - Bitmap bm = BitmapFactory.decodeResource(getResources(), - R.drawable.kale); - Palette palette = Palette.generate(bm); - if (palette.getLightVibrantSwatch() != null) { - //得到不同的样本,设置给imageview进行显示 - iv.setBackgroundColor(palette.getLightVibrantSwatch().getRgb()); - iv1.setBackgroundColor(palette.getDarkVibrantSwatch().getRgb()); - iv2.setBackgroundColor(palette.getLightMutedSwatch().getRgb()); - iv3.setBackgroundColor(palette.getDarkMutedSwatch().getRgb()); - } - ``` - -使用DrawerLayout ---- - -- 布局中的使用 - -```xml - - - - - - - - - - - - - - - - - -``` - -使用DrawerLayout后可以实现类似SlidingMenu的效果。但是怎么将DrawerLayout与ToolBar结合起来呢? 还有再结合Navigation Tabs -以及ViewPager。下面我就直接上代码了。 - -先看布局: activity_main.xml -```xml - - - - - - - - - - - - - - - - - -``` - -MainActivity的代码: -```java -public class MainActivity extends AppCompatActivity { - - private Context mContext; - - private Toolbar mToolbar; - private PagerSlidingTabStrip mScrollingTabs; - private ViewPager mViewPager; - private MainPagerAdapter mPagerAdapter; - private ActionBarDrawerToggle mDrawerToggle; - private DrawerLayout mDrawerLayout; - - private List mTitles; - private List mFragments; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - mContext = this; - - findView(); - setToolBar(); - initView(); - initDrawerFragment(); - } - - private void findView() { - mToolbar = (Toolbar) findViewById(R.id.toolbar); - mScrollingTabs = (PagerSlidingTabStrip) findViewById(R.id.psts_main); - mViewPager = (ViewPager) findViewById(R.id.vp_main); - mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); - } - - - private void setToolBar() { - mToolbar.setTitle(R.string.app_name); - setSupportActionBar(mToolbar); - getSupportActionBar().setDisplayShowHomeEnabled(true); - } - - private void initView() { - mFragments = new ArrayList<>(); - for (int xxx = 0; xxx < 5; xxx++) { - mFragments.add(new FriendsFragment()); - } - - mTitles = new ArrayList<>(); - for (int xxx = 0; xxx < 5; xxx++) { - mTitles.add("Tab : " + xxx); - } - - mPagerAdapter = new MainPagerAdapter(getSupportFragmentManager(), mFragments, mTitles); - mViewPager.setAdapter(mPagerAdapter); - mScrollingTabs.setDividerColor(Color.TRANSPARENT); - mScrollingTabs.setIndicatorHeight(10); - mScrollingTabs.setUnderlineHeight(0); - mScrollingTabs.setTextSize(50); - mScrollingTabs.setTextColor(Color.BLACK); - mScrollingTabs.setSelectedTextColor(Color.WHITE); - mScrollingTabs.setViewPager(mViewPager); - - } - - private void initDrawerFragment() { - mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout, mToolbar, R.string.drawer_open, R.string.drawer_close) { - @Override - public void onDrawerOpened(View drawerView) { - super.onDrawerOpened(drawerView); - MainActivity.this.invalidateOptionsMenu(); - } - - @Override - public void onDrawerClosed(View drawerView) { - super.onDrawerClosed(drawerView); - MainActivity.this.invalidateOptionsMenu(); - } - - @Override - public void onDrawerSlide(View drawerView, float slideOffset) { - super.onDrawerSlide(drawerView, slideOffset); - mToolbar.setAlpha(1 - slideOffset / 2); - } - }; - - mDrawerLayout.setDrawerListener(mDrawerToggle); - mDrawerToggle.syncState(); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - // Inflate the menu; this adds items to the action bar if it is present. - getMenuInflater().inflate(R.menu.menu_main, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle action bar item clicks here. The action bar will - // automatically handle clicks on the Home/Up button, so long - // as you specify a parent activity in AndroidManifest.xml. - int id = item.getItemId(); - - //noinspection SimplifiableIfStatement - if (id == R.id.action_settings) { - return true; - } - if (id == R.id.action_search) { - Toast.makeText(getApplicationContext(), "Search action is selected!", Toast.LENGTH_SHORT).show(); - return true; - } - return super.onOptionsItemSelected(item); - } - -} -``` -最后再看一下`DrawerFragment`的代码: -```java -public class DrawerFragment extends Fragment { - private Context mContext; - private RecyclerView mRecyclerView; - private NavigationDrawerAdapter mAdapter; - private static String[] titles = null; - - public DrawerFragment() { - - } - - public static List getData() { - List data = new ArrayList<>(); - - // preparing navigation drawer items - for (int i = 0; i < titles.length; i++) { - NavDrawerItem navItem = new NavDrawerItem(); - navItem.setTitle(titles[i]); - data.add(navItem); - } - return data; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mContext = getActivity(); - // drawer labels - titles = getActivity().getResources().getStringArray(R.array.nav_drawer_labels); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - // Inflating view layout - View layout = inflater.inflate(R.layout.fragment_navigation_drawer, container, false); - mRecyclerView = (RecyclerView) layout.findViewById(R.id.drawerList); - mRecyclerView.setHasFixedSize(true); - mAdapter = new NavigationDrawerAdapter(getActivity(), getData()); - mRecyclerView.setAdapter(mAdapter); - mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); - mAdapter.setOnRecyclerViewListener(new NavigationDrawerAdapter.OnRecyclerViewListener() { - @Override - public void onItemClick(int position) { - Toast.makeText(mContext, getData().get(position).getTitle(), Toast.LENGTH_SHORT).show(); - startActivity(new Intent(getActivity(), FriendsActivity.class)); - } - - @Override - public boolean onItemLongClick(int position) { - return false; - } - }); - return layout; - } - -} -``` -上面的`PagerSlidingTabStrip`是开源项目,我改了下,添加了一个选中时的文字颜色改变。 - -[Demo地址](https://github.com/CharonChui/MaterialLibrary) - -### NavigationView - -NavigationView包含两个部分:menu和headerLayout。menu是用来在NavigationView中显示具体的菜单项的,headerLayout则是用来在NavigationView中显示头部布局的。 - -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/navigator_view.png?raw=true) - -```xml - - - - - - - - - - - -``` - - - -drawer_header.xml 如下 - -```xml - - - - - - - - -``` - - - -drawer_view.xml 如下: - -```xml - - - - - - - - - - - - - - - - -``` - - - -我们给 Header 和 Menu 添加点击事件: - -```java -final NavigationView navigationView = (NavigationView) findViewById(R.id.navigation_view); - navigationView.getHeaderView(0).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - drawerLayout.closeDrawer(navigationView); - Toast.makeText(MainActivity.this, "Header View is clicked!", Toast.LENGTH_SHORT).show(); - } - }); - navigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() { - @Override - public boolean onNavigationItemSelected(@NonNull MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_home: - Toast.makeText(MainActivity.this, "Home is clicked!", Toast.LENGTH_SHORT).show(); - break; - case R.id.menu_settings: - Toast.makeText(MainActivity.this, "Settings is clicked!", Toast.LENGTH_SHORT).show(); - break; - case R.id.menu_share: - Toast.makeText(MainActivity.this, "Share is clicked!", Toast.LENGTH_SHORT).show(); - break; - case R.id.menu_about: - Toast.makeText(MainActivity.this, "About is clicked!", Toast.LENGTH_SHORT).show(); - break; - } - drawerLayout.closeDrawer(navigationView); - return false; - } - }); -``` - - - -### CoordinatorLayout - -**一、CoordinatorLayout 的作用** - -从名字可以看出,这个ViewGroup是用来协调它的子View的,CoordinatorLayout 作为一个 **“super-powered FrameLayout”**,主要有以下两个作用: - -1. 作为顶层布局; -2. 作为协调子View之间交互的容器。 - -CoordinatorLayout也是在`com.android.support.design`包中的组件。 - -通过为 CoordinatorLayout 的子View指定 Behaviors,你可以在单一父View下提供许多不同的交互,同时也可以让子View间各自进行交互。 -#### CoordinatorLayout与FloadingActionButton - -```xml - - - - -``` - - - -```java -public void onClick(View v) { - switch (v.getId()) { - case R.id.fab: - Snackbar.make(findViewById(R.id.contentView), "Snackbar", Snackbar.LENGTH_SHORT).show(); - break; - } -} -``` - -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/smacker_floatingbutton.webp?raw=true) - -可以看到FloatingActionButton会被SmackBar遮挡,为了解决遮挡的问题,就需要使用到CoordinatorLayout。CoordinatorLayout可以说是一个加强版的FrameLayout,这个布局也是由Design Support库提供的。它在普通情况下的作用和FrameLayout基本一致,不过既然是Design Support库中提供的布局,那么就必然有一些Material Design的魔力了。 -事实上,CoordinatorLayout可以监听其所有子控件的各种事件,然后自动帮助我们做出最为合理的响应。举个简单的例子,刚才弹出的Snackbar提示将悬浮按钮遮挡住了,而如果我们能让CoordinatorLayout监听到Snackbar的弹出事件,那么它会自动将内部的FloatingActionButton向上偏移,从而确保不会被Snackbar遮挡到。 - -```xml - - - - - -``` - - - -悬浮按钮自动向上偏移了Snackbar的同等高度,从而确保不会被遮挡住,当Snackbar消失的时候,悬浮按钮会自动向下偏移回到原来位置。 -另外悬浮按钮的向上和向下偏移也是伴随着动画效果的,且和Snackbar完全同步,整体效果看上去特别赏心悦目。 - - - -或者我们也可以不把不过FloatingActionButton放到布局中,只是make()方法时传入的view是在布局中即可,我们回过头来再思考一下,刚才说的是CoordinatorLayout可以监听其所有子控件的各种事件,但是Snackbar好像并不是CoordinatorLayout的子控件吧,为什么它却可以被监听到呢? - -其实道理很简单,还记得我们在Snackbar的make()方法中传入的第一个参数吗?这个参数就是用来指定Snackbar是基于哪个View来触发的,刚才我们传入的是FloatingActionButton本身,而FloatingActionButton是CoordinatorLayout中的子控件,因此这个事件就理所应当能被监听到了。你可以自己再做个试验,如果给Snackbar的make()方法传入一个DrawerLayout,那么Snackbar就会再次遮挡住悬浮按钮,因为DrawerLayout不是CoordinatorLayout的子控件,CoordinatorLayout也就无法监听到Snackbar的弹出和隐藏事件了。 - - - -#### CollapsingToolbarLayout - -可折叠式标题栏,CollapsingToolbarLayout是一个作用于Toolbar基础之上的布局,它也是由Design Support库提供的。CollapsingToolbarLayout可以让Toolbar的效果变得更加丰富,不仅仅是展示一个标题栏,而是能够实现非常华丽的效果。 - - - -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/CollapsingToolbarLayout.gif?raw=true) - - - -可以看到,我们在CollapsingToolbarLayout中定义了一个ImageView和一个Toolbar,也就意味着,这个高级版的标题栏将是由普通的标题栏加上图片组合而成的。这里定义的大多数属性我们都是见过的,就不再解释了,只有一个app:layout_collapseMode比较陌生。它用于指定当前控件在CollapsingToolbarLayout折叠过程中的折叠模式,其中Toolbar指定成pin,表示在折叠的过程中位置始终保持不变,ImageView指定成parallax,表示会在折叠的过程中产生一定的错位偏移,这种模式的视觉效果会非常好。 - -### NestedScrollView - -NestedScrollView 即 支持嵌套滑动的ScrollView。 - -因此,我们可以简单的把NestedScrollView类比为ScrollView,其作用就是作为控件父布局,从而具备(嵌套)滑动功能。 - -NestedScrollView与ScrollView的区别就在于NestedScrollView支持 *嵌套滑动*,无论是作为父控件还是子控件,嵌套滑动都支持,且默认开启。 - -因此,在一些需要支持嵌套滑动的情景中,比如一个ScrollView内部包裹一个RecyclerView,那么就会产生滑动冲突,这个问题就需要你自己去解决。而如果使用NestedScrollView包裹RecyclerView,嵌套滑动天然支持,你无需做什么就可以实现前面想要实现的功能了。 - -我们通常为RecyclerView增加一个Header和Footer的方法是通过定义不同的viewType来区分的,而如果使用NestedScrollView,我们完全可以把RecyclerView当成一个单独的控件,然后在其上面增加一个控件作为Header,在其下面增加一个控件作为Footer。 - -虽然NestedScrollView内嵌RecyclerView和其他控件可以实现Header和Footer,但还是不推荐上面这种做法(建议还是直接使用RecyclerView自己添加Header和Footer),因为虽然NestedScrollView支持嵌套滑动,但是在实际应用中,嵌套滑动可能会带来其他的一些奇奇怪怪的副作用,Google 也推荐我们能不使用嵌套滑动就尽量不要使用。 - - - -作者:Whyn -链接:https://www.jianshu.com/p/f55abc60a879 -来源:简书 -著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 - - - - - - - - -Ripple效果 ---- - -个人非常喜欢的效果。相当于给点击事件加上了动态的赶脚。。。 - - -假设现在有一个`Button`的`selector`,我们想给这个`Button`加上`Ripple`效果,肿么办? -新建一个`xml`文件,用`ripple`包裹`selector`,然后在`Button`的`backgroud`直接引用这个`xml`就好了。 -```xml - - - - - - - - -``` -但是很遗憾,`ripple`是5.0才有的,而且`support`包中没有实现该功能的扩展。 -`5.0`的这些效果还是无法在低版本上实现,包括一些`TextView`等样式,现在可以用大神的开源项目 -[MaterialDesignLibrary](https://github.com/navasmdc/MaterialDesignLibrary) - - -RecyclerView ---- - -`ListView`的升级版,还有什么理由不去用呢? 同样他也在`support v7`包中。 -``` -compile 'com.android.support:recyclerview-v7:21.+' -``` -通过`mRecyclerView.setLayoutManager(new LinearLayoutManager(this)); `设置为`LinearLayoutManager`来实现水平或者竖直 -方向的`ListView`。 - - -阴影 ---- - -通过对`View`设置`backgroud`后再添加`android:elevation="2dp"`来实现背景大小。 - - - -[更多实例请看MaterialSample](https://github.com/CharonChui/MaterialSample.git) - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! diff --git "a/ArchitectureComponents/2.\351\233\206\346\210\220(\344\272\214).md" "b/ArchitectureComponents/2.\351\233\206\346\210\220(\344\272\214).md" deleted file mode 100644 index 396665c9..00000000 --- "a/ArchitectureComponents/2.\351\233\206\346\210\220(\344\272\214).md" +++ /dev/null @@ -1,164 +0,0 @@ -2.集成(二) -=== - - -首先在`Project`目录中的`build.gradle`中添加`google()`仓库(大部分项目可能都已经有了): - -``` -allprojects { - repositories { - jcenter() - google() - } -} -``` - -然后在`app`的`build.gradle`中添加对应的依赖,如: - -``` -implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version" -``` -如果想要使用`kotlin`开发的话,可以在后面加上`-ktx`后缀就可以了,如下: -``` -implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" -``` - -`Lifecycle`依赖: ---- - -`Lifecycle`依赖包括`LiveData`和`ViewModel` - -``` -dependencies { - def lifecycle_version = "1.1.1" - - // ViewModel and LiveData - implementation "android.arch.lifecycle:extensions:$lifecycle_version" - // alternatively - just ViewModel - implementation "android.arch.lifecycle:viewmodel:$lifecycle_version" // use -ktx for Kotlin - // alternatively - just LiveData - implementation "android.arch.lifecycle:livedata:$lifecycle_version" - // alternatively - Lifecycles only (no ViewModel or LiveData). - // Support library depends on this lightweight import - implementation "android.arch.lifecycle:runtime:$lifecycle_version" - - annotationProcessor "android.arch.lifecycle:compiler:$lifecycle_version" - // alternately - if using Java8, use the following instead of compiler - implementation "android.arch.lifecycle:common-java8:$lifecycle_version" - - // optional - ReactiveStreams support for LiveData - implementation "android.arch.lifecycle:reactivestreams:$lifecycle_version" - - // optional - Test helpers for LiveData - testImplementation "android.arch.core:core-testing:$lifecycle_version" -} -``` - - -`Room`依赖: ---- - -`Room`的依赖包括`testing Room migrations`和`Room RxJava` - -``` -dependencies { - def room_version = "1.1.1" - - implementation "android.arch.persistence.room:runtime:$room_version" - annotationProcessor "android.arch.persistence.room:compiler:$room_version" - - // optional - RxJava support for Room - implementation "android.arch.persistence.room:rxjava2:$room_version" - - // optional - Guava support for Room, including Optional and ListenableFuture - implementation "android.arch.persistence.room:guava:$room_version" - - // Test helpers - testImplementation "android.arch.persistence.room:testing:$room_version" -} - -``` - -`Paging`依赖 ---- - -``` -dependencies { - def paging_version = "1.0.0" - - implementation "android.arch.paging:runtime:$paging_version" - - // alternatively - without Android dependencies for testing - testImplementation "android.arch.paging:common:$paging_version" - - // optional - RxJava support, currently in release candidate - implementation "android.arch.paging:rxjava2:1.0.0-rc1" -} -``` - -`Navigation`依赖 ---- - -> Navigation classes are already in the androidx.navigation package, but currently depend on Support Library 27.1.1, and associated Arch component versions. Version of Navigation with AndroidX dependencies will be released in the future. - -``` -dependencies { - def nav_version = "1.0.0-alpha02" - - implementation "android.arch.navigation:navigation-fragment:$nav_version" // use -ktx for Kotlin - implementation "android.arch.navigation:navigation-ui:$nav_version" // use -ktx for Kotlin - - // optional - Test helpers - androidTestImplementation "android.arch.navigation:navigation-testing:$nav_version" // use -ktx for Kotlin -} -``` - - -`Safe args`依赖 ---- - -想要使用`Safe args`,需要在`Project`顶层的`build.gradle`中配置以下路径: -``` -buildscript { - repositories { - google() - } - dependencies { - classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0-alpha02" - } -} -``` -并且在`app`或`module`中`build.gradle`中: -``` -apply plugin: "androidx.navigation.safeargs" -``` - -`WorkManager`依赖 ---- - -> WorkManager classes are already in the androidx.work package, but currently depend on Support Library 27.1, and associated Arch component versions. Version of WorkManager with AndroidX dependencies will be released in the future. - - -``` -dependencies { - def work_version = "1.0.0-alpha03" - - implementation "android.arch.work:work-runtime:$work_version" // use -ktx for Kotlin - - // optional - Firebase JobDispatcher support - implementation "android.arch.work:work-firebase:$work_version" - - // optional - Test helpers - androidTestImplementation "android.arch.work:work-testing:$work_version" -} -``` - - -[上一篇: 1.简介(一)](https://github.com/CharonChui/AndroidNote/blob/master/ArchitectureComponents/1.%E7%AE%80%E4%BB%8B(%E4%B8%80).md) -[下一篇: 3.Lifecycle(三)](https://github.com/CharonChui/AndroidNote/blob/master/ArchitectureComponents/3.Lifecycle(%E4%B8%89).md) - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! ` \ No newline at end of file diff --git "a/ArchitectureComponents/3.Lifecycle(\344\270\211).md" "b/ArchitectureComponents/3.Lifecycle(\344\270\211).md" deleted file mode 100644 index b034723b..00000000 --- "a/ArchitectureComponents/3.Lifecycle(\344\270\211).md" +++ /dev/null @@ -1,175 +0,0 @@ -3.Lifecycle(三) -=== - - -`Android`开发中,经常需要管理生命周期。举个栗子,我们需要获取用户的地址位置,当这个`Activity`在显示的时候,我们开启定位功能,然后实时获取到定位信息,当页面被销毁的时候,需要关闭定位功能。 -```java -class MyLocationListener { - public MyLocationListener(Context context, Callback callback) { - // ... - } - - void start() { - // connect to system location service - } - - void stop() { - // disconnect from system location service - } -} - - -class MyActivity extends AppCompatActivity { - private MyLocationListener myLocationListener; - - @Override - public void onCreate(...) { - myLocationListener = new MyLocationListener(this, (location) -> { - // update UI - }); - } - - @Override - public void onStart() { - super.onStart(); - myLocationListener.start(); - // manage other components that need to respond - // to the activity lifecycle - } - - @Override - public void onStop() { - super.onStop(); - myLocationListener.stop(); - // manage other components that need to respond - // to the activity lifecycle - } -} -``` - -上面的代码看起来还挺简单,但是当定位功能需要满足一些条件下才开启,那么会变得复杂多了。可能在执行`Activity`的`stop`方法时,定位的`start`方法才刚刚开始执行,比如如下代码,这样生命周期管理就变得很麻烦了。 -```java -class MyActivity extends AppCompatActivity { - private MyLocationListener myLocationListener; - - public void onCreate(...) { - myLocationListener = new MyLocationListener(this, location -> { - // update UI - }); - } - - @Override - public void onStart() { - super.onStart(); - Util.checkUserStatus(result -> { - // what if this callback is invoked AFTER activity is stopped? - if (result) { - myLocationListener.start(); - } - }); - } - - @Override - public void onStop() { - super.onStop(); - myLocationListener.stop(); - } -} -``` - -`android.arch.lifecycle`包提供的类和接口可帮助您用简单和独立的方式解决这些问题。 - -`Lifecycle`类是一个持有组件(`activity`或`fragment`)生命周期信息的类,其他对象可以观察该状态。`Lifecycle`使用两个重要的枚举部分来管理对应组件的生命周期的状态: - -- `Event`:生命周期事件由系统来分发,这些事件对应于`Activity`和`Fragment`的生命周期函数。 - -- `State`:`Lifecycle`对象所追踪的组件的当前状态 - - - - -```kotlin -class MainActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - // lifecycle是LifecycleOwner接口的getLifecycle()方法得到的,从com.android.support:appcompat-v7:26.1.0开始activity和fragment都实现了该接口 - lifecycle.addObserver(MyObserver()) - } -} -``` - -```kotlin -class MyObserver : LifecycleObserver{ - @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) - fun connectListener() { - Log.e("@@@", "connect") - } - - @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) - fun disconnectListener() { - Log.e("@@@", "disconnect") - } -} -``` -上面的`lifecycle.addObserver(MyObserver()) `的完整写法应该是`aLifecycleOwner.getLifecycle().addObserver(new MyObserver())`而`aLifecycleOwner`一般是实现了`LifecycleOwner`的类,比如`Activity/Fragment` - - - -`LifecycleOwner` ---- - -那什么是`LifecycleOwner`呢?实现`LifecycleOwner`接口就表示这是个有生命周期的类,他有一个`getLifecycle ()`方法是必须实现的。 - -对于前面提到的监听位置的例子。可以把`MyLocationListener`实现`LifecycleObserver`,然后在`Lifecycle(Activity/Fragment)`的`onCreate`方法中初始化。这样`MyLocationListener`就能自行处理生命周期带来的问题。 - - - -从`Support Library 26.1.0`开始`Activity/Fragment`已经实现了`LifecycleOwner`接口。 -如果想在自定义的类中实现`LifecyclerOwner`,就需要用到[LifecycleRegistry](https://developer.android.com/reference/android/arch/lifecycle/LifecycleRegistry)类,并且需要自行发送`Event`: - -```java -public class MyActivity extends Activity implements LifecycleOwner { - private LifecycleRegistry mLifecycleRegistry; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - mLifecycleRegistry = new LifecycleRegistry(this); - mLifecycleRegistry.markState(Lifecycle.State.CREATED); - } - - @Override - public void onStart() { - super.onStart(); - mLifecycleRegistry.markState(Lifecycle.State.STARTED); - } - - @NonNull - @Override - public Lifecycle getLifecycle() { - return mLifecycleRegistry; - } -} -``` - - -`Lifecycles`的最佳建议: - -- 保持`UI Controllers(Activity/Fragment)`中代码足够简洁。一定不能包含如何获取数据的代码,要通过`ViewModel`获取`LiveData`形式的数据。 -- 用数据驱动`UI`,`UI`的职责就是根据数据改变显示的内容,并且把用户操作`UI`的行为传递给`ViewModel`。 -- 把业务逻辑相关的代码放到`ViewModel`中,把`ViewModel`看成是链接`UI`和`App`其他部分的纽带。但`ViewModel`不能直接获取数据,要通过调用其他类来获取数据。 -- 使用`DataBinding`来简化`View`(布局文件)和`UI Controllers(Activity/Fragment)`之间的代码 -- 如果布局本身太过复杂,可以考虑创建一个`Presenter`类来处理UI相关的改变。虽然这么做会多写很多代码,但是对于保持`UI`的简介和可测试性是有帮助的。 -- 不要在`ViewModel`中持有任何`View/Activity`的`context`。否则会造成内存泄露。 - - -[上一篇: 2.集成(二)](https://github.com/CharonChui/AndroidNote/blob/master/ArchitectureComponents/2.%E9%9B%86%E6%88%90(%E4%BA%8C).md) -[下一篇: 4.LiveData(四)](https://github.com/CharonChui/AndroidNote/blob/master/ArchitectureComponents/4.LiveData(%E5%9B%9B).md) - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! ` \ No newline at end of file diff --git "a/ArchitectureComponents/4.LiveData(\345\233\233).md" "b/ArchitectureComponents/4.LiveData(\345\233\233).md" deleted file mode 100644 index 143fdc3f..00000000 --- "a/ArchitectureComponents/4.LiveData(\345\233\233).md" +++ /dev/null @@ -1,315 +0,0 @@ -4.LiveData(四) -=== - -> LiveData is an observable data holder class. Unlike a regular observable, LiveData is lifecycle-aware, meaning it respects the lifecycle of other app components, such as activities, fragments, or services. This awareness ensures LiveData only updates app component observers that are in an active lifecycle state. - - -`LiveData`是一种持有可被观察数据的类。和其他可被观察的类不同的是,`LiveData`是有生命周期感知能力的,这意味着它可以在`activities`,`fragments`,或者`services`生命周期是活跃状态时更新这些组件。那么什么是活跃状态呢?上篇文章中提到的`STARTED`和`RESUMED`就是活跃状态,只有在这两个状态下`LiveData`是会通知数据变化的。 - -要想使用`LiveData`(或者这种有可被观察数据能力的类)就必须配合实现了`LifecycleOwner`的对象使用。在这种情况下,当对应的生命周期对象`DESTROYED`时,才能移除观察者。这对`Activity`或者`Fragment`来说显得尤为重要,因为他们可以在生命周期结束的时候立刻解除对数据的订阅,从而避免内存泄漏等问题。 - - - - -使用`LiveData`的优点: - -- `UI`和实时数据保持一致 因为`LiveData`采用的是观察者模式,这样一来就可以在数据发生改变时获得通知,更新`UI`。 -- 避免内存泄漏,观察者被绑定到组件的生命周期上,当被绑定的组件销毁(`destory`)时,观察者会立刻自动清理自身的数据。 -- 不会再产生由于`Activity`处于`stop`状态而引起的崩溃 例如:当`Activity`处于后台状态时,是不会收到`LiveData`的任何事件的。 -- 不需要再解决生命周期带来的问题`LiveData`可以感知被绑定的组件的生命周期,只有在活跃状态才会通知数据变化。 -- 实时数据刷新,当组件处于活跃状态或者从不活跃状态到活跃状态时总是能收到最新的数据 -- 解决`Configuration Change`问题,在屏幕发生旋转或者被回收再次启动,立刻就能收到最新的数据。 -- 数据共享,如果对应的`LiveData`是单例的话,就能在`app`的组件间分享数据。 - - - -使用`LiveData`: - -- 创建一个持有某种数据类型的`LiveData`(通常是在`ViewModel`中) -- 创建一个定义了`onChange()`方法的观察者。这个方法是控制`LiveData`中数据发生变化时,采取什么措施 (比如更新界面)。通常是在`UI Controller`(`Activity/Fragment`)中创建这个观察者。 -- 通过`observe()`方法连接观察者和`LiveData`。`observe()`方法需要携带一个`LifecycleOwner`类。这样就可以让观察者订阅`LiveData`中的数据,实现实时更新。 - - -创建`LiveData`对象 ---- - -`LiveData`是一个数据的包装。具体的包装对象可以是任何数据,包括集合(比如`List`)。`LiveData`通常在`ViewModel`中创建,然后通过`getter`方法获取。具体可以看一下代码: -```java -public class NameViewModel extends ViewModel { - -// Create a LiveData with a String -private MutableLiveData mCurrentName; - - public MutableLiveData getCurrentName() { - if (mCurrentName == null) { - mCurrentName = new MutableLiveData(); - } - return mCurrentName; - } - -// Rest of the ViewModel... -} -``` - -观察`LiveData`中的数据 ---- - - -通常情况下都是在组件的`onCreate()`方法中开始观察数据,原因有以下两点: - -- 系统会多次调用`onResume()`方法 -- 确保`Activity/Fragment`在处于活跃状态时立刻可以展示数据。 - -```java -public class NameActivity extends AppCompatActivity { - - private NameViewModel mModel; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - // Other code to setup the activity... - - // Get the ViewModel. - mModel = ViewModelProviders.of(this).get(NameViewModel.class); - - - // Create the observer which updates the UI. - final Observer nameObserver = new Observer() { - @Override - public void onChanged(@Nullable final String newName) { - // Update the UI, in this case, a TextView. - mNameTextView.setText(newName); - } - }; - - // Observe the LiveData, passing in this activity as the LifecycleOwner and the observer. - mModel.getCurrentName().observe(this, nameObserver); - } -} -``` - -更新`LiveData`对象 ---- - - -如果想要在`UI Controller`中改变`LiveData`中的值呢?(比如点击某个`Button`把性别从男设置成女)。`LiveData`并没有提供这样的功能,但是`Architecture Component`提供了`MutableLiveData`这样一个类,可以通过`setValue(T)`和`postValue(T)`方法来修改存储在`LiveData`中的数据。`MutableLiveData`是`LiveData`的一个子类,从名称上也能看出这个类的作用。举个直观点的例子: - -```java -mButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - String anotherName = "John Doe"; - mModel.getCurrentName().setValue(anotherName); - } -}) -``` -调用`setValue()`方法就可以把`LiveData`中的值改为`John Doe`。同样通过这种方法修改`LiveData`中的值同样会触发所有对这个数据感兴趣的类。那么`setValue()`和`postValue()`有什么不同呢?区别就是`setValue()`只能在主线程中调用,而`postValue()`可以在子线程中调用。 - - -`Room`和`LiveData`配合使用 ---- - -`Room`可以返回`LiveData`的数据类型。这样对数据库中的任何改动都会被传递出去。这样修改完数据库就能获取最新的数据,减少了主动获取数据的代码。 - -继承`LiveData`扩展功能 ---- - -`LiveData`的活跃状态包括:`STARTED`或者`RESUMED`两种状态。那么如何在活跃状态下把数据传递出去呢?下面是示例代码: - -```java -public class StockLiveData extends LiveData { - private StockManager mStockManager; - - private SimplePriceListener mListener = new SimplePriceListener() { - @Override - public void onPriceChanged(BigDecimal price) { - setValue(price); - } - }; - - public StockLiveData(String symbol) { - mStockManager = new StockManager(symbol); - } - - @Override - protected void onActive() { - mStockManager.requestPriceUpdates(mListener); - } - - @Override - protected void onInactive() { - mStockManager.removeUpdates(mListener); - } -} -``` - -上面有三个重要的方法: - -- The onActive() method is called when the LiveData object has an active observer. This means you need to start observing the stock price updates from this method. -- The onInactive() method is called when the LiveData object doesn't have any active observers. Since no observers are listening, there is no reason to stay connected to the StockManager service. -- The setValue(T) method updates the value of the LiveData instance and notifies any active observers about the change. - -可以像下面这样使用`StockLiveData`: -```java -public class MyFragment extends Fragment { - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - LiveData myPriceListener = ...; - myPriceListener.observe(this, price -> { - // Update the UI. - }); - } -} -``` -上面`observe()`方法中的第一个参数传递的是`fragment`的实例,该`fragment`实现了`LifecycleOwner`接口。这样做是为了将`observer`和`Lifecycle`对象绑定到一起,这意味着: -- 如果当前的`Lifecycle`对象不是出于活跃期,就算`value`值有改变也不会回调到`observer`中 -- 在`Lifecycle`对象销毁后哦,`observer`对象也会自动移除 - -实际上`LiveData`对象是适应生命周期也就意味着你需要在多个`activities`,`fragments`和`services`中进行共享,所以通常我们会将`LiveData`的示例设计成单例的: -```java -public class StockLiveData extends LiveData { - private static StockLiveData sInstance; - private StockManager mStockManager; - - private SimplePriceListener mListener = new SimplePriceListener() { - @Override - public void onPriceChanged(BigDecimal price) { - setValue(price); - } - }; - - @MainThread - public static StockLiveData get(String symbol) { - if (sInstance == null) { - sInstance = new StockLiveData(symbol); - } - return sInstance; - } - - private StockLiveData(String symbol) { - mStockManager = new StockManager(symbol); - } - - @Override - protected void onActive() { - mStockManager.requestPriceUpdates(mListener); - } - - @Override - protected void onInactive() { - mStockManager.removeUpdates(mListener); - } -} -``` -这样就可以在`fragment`中像如下这样使用: -```java -public class MyFragment extends Fragment { - @Override - public void onActivityCreated(Bundle savedInstanceState) { - StockLiveData.get(getActivity()).observe(this, price -> { - // Update the UI. - }); - } -} -``` - -转换LiveData ---- - -你可能有时会在`LiveData`分发给`observers`之前想要修改一下存储在`LiveData`中的值,或者你想根据当前的值进行修改返回另一个值。`Lifecycle`提供了`Transformations`类来通过里面的`helper`方法解决这种问题。 - -- `Transformations.map()` - -可以将`LiveData`中的数据进行改变。 - - -```java -LiveData userLiveData = ...; -LiveData userName = Transformations.map(userLiveData, user -> { - user.name + " " + user.lastName -}); -``` -将`LiveData`中的`User`数据转换成`String` - - -- `Transformations.switchMap()` - -```java -private LiveData getUser(String id) { - ...; -} - -LiveData userId = ...; -LiveData user = Transformations.switchMap(userId, id -> getUser(id) ); -``` - - -和上面的`map()`方法很像。区别在于传递给`switchMap()`的函数必须返回`LiveData`对象。 -和`LiveData`一样,`Transformation`也可以在观察者的整个生命周期中存在。只有在观察者处于观察`LiveData`状态时,`Transformation`才会运算。`Transformation`是延迟运算的(`calculated lazily`),而生命周期感知的能力确保不会因为延迟发生任何问题。 - -如果在`ViewModel`对象的内部需要一个`Lifecycle`对象,那么使用`Transformation`是一个不错的方法。举个例子:假如有个`UI`组件接受输入的地址,返回对应的邮政编码。那么可以 实现一个`ViewModel`和这个组件绑定: -```java -class MyViewModel extends ViewModel { - private final PostalCodeRepository repository; - public MyViewModel(PostalCodeRepository repository) { - this.repository = repository; - } - - private LiveData getPostalCode(String address) { - // DON'T DO THIS - return repository.getPostCode(address); - } -} - -``` - -看代码中的注释,有个`// DON'T DO THIS`(不要这么干),这是为什么?有一种情况是如果`UI`组件被回收后又被重新创建,那么又会触发一次`repository.getPostCode(address)`询,而不是重用上次已经获取到的查询。那么应该怎样避免这个问题呢?看一下下面的代码: - -```java -class MyViewModel extends ViewModel { - private final PostalCodeRepository repository; - private final MutableLiveData addressInput = new MutableLiveData(); - public final LiveData postalCode = - Transformations.switchMap(addressInput, (address) -> { - return repository.getPostCode(address); - }); - - public MyViewModel(PostalCodeRepository repository) { - this.repository = repository - } - - private void setInput(String address) { - addressInput.setValue(address); - } -} -``` - -`postalCode`变量的修饰符是`public`和`final`,因为这个变量的是不会改变的。哎?不会改变?那我输入不同的地址还总返回相同邮编?先打住,`postalCode`这个变量存在的作用是把输入的`addressInput`转换成邮编,那么只有在输入变化时才会调用`repository.getPostCode()`方法。这就好比你用`final`来修饰一个数组,虽然这个变量不能再指向其他数组,但是数组里面的内容是可以被修改的。绕来绕去就一点:当输入是相同的情况下,用了`switchMap()`可以减少没有必要的请求。并且同样,只有在观察者处于活跃状态时才会运算并将结果通知观察者。 - - - - -合并多个`LiveData`中的数据 ---- - -`MediatorLiveData`是`LiveData`的子类,可以通过`MediatorLiveData`合并多个`LiveData`来源的数据。同样任意一个来源的`LiveData`数据发生变化,`MediatorLiveData`都会通知观察他的对象。说的有点抽象,举个例子。比如`UI`接收来自本地数据库和网络数据,并更新相应的`UI`。可以把下面两个`LiveData`加入到`MeidatorLiveData`中: - -- 关联数据库的`LiveData` -- 关联联网请求的`LiveData` -相应的`UI`只需要关注`MediatorLiveData`就可以在任意数据来源更新时收到通知。 - - - -[上一篇: 3.Lifecycle(三)](https://github.com/CharonChui/AndroidNote/blob/master/ArchitectureComponents/3.Lifecycle(%E4%B8%89).md) -[下一篇: 5.ViewModel(五)](https://github.com/CharonChui/AndroidNote/blob/master/ArchitectureComponents/5.ViewModel(%E4%BA%94).md) - - - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! ` \ No newline at end of file diff --git "a/ArchitectureComponents/5.ViewModel(\344\272\224).md" "b/ArchitectureComponents/5.ViewModel(\344\272\224).md" deleted file mode 100644 index 98ea2f37..00000000 --- "a/ArchitectureComponents/5.ViewModel(\344\272\224).md" +++ /dev/null @@ -1,138 +0,0 @@ -5.ViewModel(五) -=== - -`ViewModel`是用来存储`UI`层的数据,以及管理对应的数据,当数据修改的时候,可以马上刷新`UI`。 - -`Android`系统提供控件,比如`Activity`和`Fragment`,这些控件都是具有生命周期方法,这些生命周期方法被系统调用。 - -当这些控件被销毁或者被重建的时候,如果数据保存在这些对象中,那么数据就会丢失。比如在一个界面,保存了一些用户信息,当界面重新创建的时候,就需要重新去获取数据。当然了也可以使用控件自动再带的方法,在`onSaveInstanceState`方法中保存数据,在`onCreate`中重新获得数据,但这仅仅在数据量比较小的情况下。如果数据量很大,这种方法就不能适用了。 - -另外一个问题就是,经常需要在`Activity`中加载数据,这些数据可能是异步的,因为获取数据需要花费很长的时间。那么`Activity`就需要管理这些数据调用,否则很有可能会产生内存泄露问题。最后需要做很多额外的操作,来保证程序的正常运行。 - -同时`Activity`不仅仅只是用来加载数据的,还要加载其他资源,做其他的操作,最后`Activity`类变大,就是我们常讲的上帝类。也有不少架构是把一些操作放到单独的类中,比如`MVP`就是这样,创建相同类似于生命周期的函数做代理,这样可以减少`Activity`的代码量,但是这样就会变得很复杂,同时也难以测试。 - -`AAC`中提供`ViewModel`可以很方便的用来管理数据。我们可以利用它来管理`UI`组件与数据的绑定关系。`ViewModel`提供自动绑定的形式,当数据源有更新的时候,可以自动立即的更新`UI`。 - - -实现`ViewModel` ---- - -```java -public class MyViewModel extends ViewModel { - private MutableLiveData> users; - public LiveData> getUsers() { - if (users == null) { - users = new MutableLiveData>(); - loadUsers(); - } - return users; - } - - private void loadUsers() { - // Do an asynchronous operation to fetch users. - } -} -``` - -然后可以再`activity`像如下这样获取数据: -```java -public class MyActivity extends AppCompatActivity { - public void onCreate(Bundle savedInstanceState) { - // Create a ViewModel the first time the system calls an activity's onCreate() method. - // Re-created activities receive the same MyViewModel instance created by the first activity. - - MyViewModel model = ViewModelProviders.of(this).get(MyViewModel.class); - model.getUsers().observe(this, users -> { - // update UI - }); - } -} -``` - -在`activity`重建后,它会收到在第一个`activity`中创建的同一个`MyViewModel`实例,当所属的`activity`销毁后,`framework`会调用`ViewModel`对象的`onCleared()` -方法来清除资源。 - - -`ViewModel`的生命周期 ---- - -`ViewModel`在获取`ViewModel`对象时会通过`ViewModelProvider`的传递来绑定对应的声明周期。 -`ViewModel`只有在`Activity finish`或者`Fragment detach`之后才会销毁。 - - - - - - -在`Fragments`间分享数据 ---- - -有时候一个`Activity`中的两个或多个`Fragment`需要分享数据或者相互通信,这样就会带来很多问题,比如数据获取,相互确定生命周期。 - -使用`ViewModel`可以很好的解决这个问题。假设有这样两个`Fragment`,一个`Fragment`提供一个列表,另一个`Fragment`提供点击每个`item`现实的详细信息。 - - -```java -public class SharedViewModel extends ViewModel { - private final MutableLiveData selected = new MutableLiveData(); - - public void select(Item item) { - selected.setValue(item); - } - - public LiveData getSelected() { - return selected; - } -} - -public class MasterFragment extends Fragment { - private SharedViewModel model; - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class); - itemSelector.setOnClickListener(item -> { - model.select(item); - }); - } -} - -public class DetailFragment extends LifecycleFragment { - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class); - model.getSelected().observe(this, { item -> - // update UI - }); - } -} -``` - -两个`Fragment`都是通过`getActivity()`来获取`ViewModelProvider`。这意味着两个`Activity`都是获取的属于同一个`Activity`的同一个`ShareViewModel`实例。 -这样做优点如下: - -- `Activity`不需要写任何额外的代码,也不需要关心`Fragment`之间的通信。 -- `Fragment`不需要处理除`SharedViewModel`以外其他的代码。这两个`Fragment`不需要知道对方是否存在。 -- `Fragment`的生命周期不会相互影响 - - - - -`ViewModel`和`SavedInstanceState`对比 ---- - -`ViewModel`使得在`configuration change`(旋转屏幕等)保存数据变的十分方便,但是这不能用于应用被系统杀死时持久化数据。举个简单的例子,有一个界面展示国家信息。 -不应该把整个国家信息放到`SavedInstanceState`里,而是把国家对应的`id`放到`SavedInstanceState`,等到界面恢复时,再通过`id`去获取详细的信息。这些详细的信息应该被存放在数据库中。 - - - - - - -[上一篇: 4.LiveData(四)](https://github.com/CharonChui/AndroidNote/blob/master/ArchitectureComponents/4.LiveData(%E5%9B%9B).md) -[下一篇: 6.Room(六)](https://github.com/CharonChui/AndroidNote/blob/master/ArchitectureComponents/6.Room(%E5%85%AD).md) - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! ` \ No newline at end of file diff --git "a/Jetpack/Jetpack\347\256\200\344\273\213.md" "b/Jetpack/Jetpack\347\256\200\344\273\213.md" new file mode 100644 index 00000000..e29d72c2 --- /dev/null +++ "b/Jetpack/Jetpack\347\256\200\344\273\213.md" @@ -0,0 +1,161 @@ +# Jetpack简介 + +JetPack是Google推出的一些库的集合。是Android基础支持库SDK以外的部分。包含了组件、工具、架构方案等...开发者可以自主按需选择接入具体的哪个库。 + +## 背景 + +### Support库 + +早之前的Android更新迭代是,所有的功能更新都是跟随着每一个特定的Android版本所发布的。 +例如: +- Fragment是在Android 3.0更新的。 +- Material Design组件是在Android 5.0上更新的 +但是由于Android用户庞大,每一次Android更新都无法覆盖所有用户的,同时因为手机厂商众多,但支持有限,也无法为自己生产的所有设备保持迭代到最新的Android版本,所以用户所持有的设备上Android版本是层次不齐的。 +从技术的角度来说,因为用户的设备版本不一致,导致Android工程师在维护项目的时候,会遇到很多难以解决的问题。为了解决这些由于Android版本不一致而出现的兼容性问题,Google推出了Support库。 + +Support库是针对Framework API的补充,Framework API跟随每一个Android版本所发布,和设备具有强关联性,Support API是开发者自主集成的,最终会包含在我们所发布的应用中,这样我们就可以在最新的Android版本上进行应用的开发,同时使用Support API解决各种潜在的兼容性问题,帮助开发者在不同Android版本的设备上实现行为一致的工作代码。 + +### Support 库的弊端 +最早的Support库发布于2011年,版本号为:android.support.v4,也就是我们所熟知的v4库,2013年在v4的基础上,Android团队发布了v7库,版本号为:android.support.v7,之后还发布了用于特定场景的v8、v13、v14、v17。 +如果是前几年刚开始学习Android的同学们,一定都对这些奇怪的数字很疑惑,4、7、8、13、14、17 到底都是什么意思? +拿第一代支持库v4举例,最初本意是指:该支持库最低可以支持到API 4(Android 1.4)的设备,v7表示最低支持 API 7(Android 2.1)的设备,但随着Android版本的持续更新,API 4以及API 7的设备早就淘汰了。在2017年7月Google将所有支持库的最低API支持版本提高到了API 14(Android 4.0),但由于包名无法修改,所以还是沿用之前的v4、v7命名标准,所以就出现了Support库第一个无法解决的问题:版本增长混乱。 + +与此同时Support库还面临一个非常严峻的问题:架构设计本身导致的严重依赖问题。最早的Support库是v4,v7是基于v4进行的补充,因为v4、v7太过庞大,功能集中,所以如果想要开发新的支持库,也只能在v4、v7的基础上进行二次开发,比方说我们后期常用的,RecyclerView、CardView等等。 + +这样就会产生很严重的重复依赖的问题,在无论是使用官方库,还是第三方库的时候,我们都需要保持整个项目中Support库版本一致,我相信很多人都在这个问题上踩过坑,虽然还是有办法解决这个问题,但无形中增加了很多工作量。 + +我们都知道“组合优于继承”这句话,但Support库在最初的架构设计上,却采用了重继承轻组合的方式,我猜这可能是因为开发Support库的人和开发Framework API的是同一批人有关,Framework API里有种各种继承逻辑,例如我们常用的 Activity、Fragment、View。 + +虽然在后期Google尝试拆分Support库,例如推出了独立的支持库:support:design、support:customtabs等,但并不能从根源解决依赖的问题。 + +### Android X +从Goole IO 2017开始。Google开始推出Architecture Component,ORM库Room,用户生命周期管理的ViewModel/LiveData. +Goole IO 2018将Support lib更名为androidx.将许多Google认为是正确的方案和实践集中起来。可以说AndroidX的出现就是为了解决长久以来Support库混乱的问题,你也可以把AndroidX理解为更强大的Support库。 +AndroidX将原有的Support库拆分为85个大大小小的支持库,抛弃了之前与API最低支持相关联的版本命名规范,重置为1.0.0,并且每一个库在之后都会按照严格的语义版本控制规则进行版本控制。 +同时通过组合依赖的方式,我们可以选择自己需要的组件库,而不是像Support一样全部依赖,一定程度上也减小了应用的体积。 +很重要的一点,就是它不会随着特定的Android版本而更新,它是由开发者自主控制,同时包含在我们所发布的应用程序中。 + + +以上种种,现在统称为JetPack. +Jetpack的出现是为了彻底解决这两个致命的问题: +1. Support 库版本增长混乱 +2. Support 库重复依赖 +如果Jetpack仅仅是针对Support库的重构,那它并没有了不起的,因为这只是Google解决了它自身因为历史原因所产生的代码问题。 +更重要的是Jetpack为大家提供了一系列的最佳实践,包含:架构、数据处理、后台任务处理、动画、UI 各个方面,无需纠结于各种因为Android本身而出现的问题,而是让我们把更多的精力放在业务需求的实现上,这才是Jetpack真正了不起的地方。其最核心的出发点就是帮助开发者快速构建出稳定、高性能、测试友好同时向后兼容的APP。 + +Jetpack相当于Google把自己的Android生态重新整理了一番。确立了Android未来的版图和大方向。 + +## 组成部分 + +前面讲到过,JetPack是一系列库和工具的集合,它更多是Google的一个提出的一个概念,或者说态度。 +并非所有的东西都是每年在IO大会上新推出的,它也包含了对现有基础库的整理和扩展。在大部分项目中其实我们都有用到JetPack的内容,也许你只是不知道而已。让我们以上帝视角来看看整个JetPack除了你熟悉的部分,还有哪些是你不熟悉但是听过的内容。看看他们都能做些什么事情。 +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_jetpack.png) + +[Jetpack完整组件列表](https://developer.android.com/jetpack/androidx/explorer) + +### Foundation(基础组件): +基础组件提供了横向功能,例如向后兼容性、测试以及Kotlin语言的支持。它包含如下组件库: +- Android KTX:Android KTX 是一组 Kotlin 扩展程序,它优化了供Kotlin使用的Jetpack和Android平台的API。以更简洁、更愉悦、更惯用的方式使用Kotlin进行Android开发。 +- AppCompat:提供了一系列以AppCompat开头的API,以便兼容低版本的Android开发。Jetpack基础中的AppCompat库包含v7库中的所有组件([支持库软件包](https://developer.android.com/topic/libraries/support-library/packages#v7-appcompat))。 其中包括AppCompat,Cardview,GridLayout,MediaRouter,Palette,RecyclerView,Renderscript,Preferences,Leanback,Vector Drawable,Design,Custom选项卡等。此外,该库为材质设计用户界面提供了实现支持,这使得AppCompat对 开发人员。 以下是android应用程序的一些关键领域,这些领域很难构建,但是可以使用AppCompat库轻松进行设计: 一般都是为了兼容 Android L以下版本,来提供Material Design的效果: + - Toolbar + - ContextCompat + - AppCompatDialog +- annotation:注解,提升代码可读性,内置了Android中常用的注解 +- Multidex(多Dex处理):为方法数超过 64K 的应用启用多 dex 文件。Security(安全):按照安全最佳做法读写加密文件和共享偏好设置。 +- Test(测试):用于单元和运行时界面测试的 Android 测试框架。 + +### Architecture(架构组件) +架构组件可帮助开发者设计稳健、可测试且易维护的应用。它包含如下组件库: +- Data Binding(数据绑定):数据绑定库是一种支持库,借助该库,可以使用声明式将布局中的界面组件绑定到应用中的数据源。 +- Lifecycles:方便管理Activity和Fragment生命周期,帮助开发者书写更轻量、易于维护的代码。 +- ViewModel:以生命周期感知的方式存储和管理与UI相关的数据。 +- LiveData:是一个可观察的数据持有者类。与常规observable不同,LiveData是有生命周期感知的。 +- Navigation:处理应用内导航所需的一切。 +- Paging:帮助开发者一次加载和显示小块数据。按需加载部分数据可减少网络带宽和系统资源的使用。 +- Room:Room持久性库在SQLite上提供了一个抽象层,帮助开发者更友好、流畅的访问SQLite数据库。 +- WorkManager:即使应用程序退出或设备重新启动,也可以轻松地调度预期将要运行的可延迟异步任务。 +- hilt:基于Dagger的Android依赖注入框架绑定View和Model +- startup:自动处理依赖初始化 +- datastore:Preferences的替代类,支持异步、更加安全 + + + +### Behavior(行为组件) +行为组件可帮助开发者的应用与标准Android服务(如通知、权限、分享和Google助理)相集成。它包含如下组件库: +- CameraX:帮助开发者简化相机应用的开发工作。它提供一致且易于使用的 API 界面,适用于大多数 Android 设备,并可向后兼容至 Android 5.0(API 级别 21)。 +- DownloadManager下载管理器:可处理长时间运行的HTTP下载,并在出现故障或在连接更改和系统重新启动后重试下载。 +- Media & playback(媒体&播放):用于媒体播放和路由(包括 Google Cast)的向后兼容 API。 +- Notifications(通知):提供向后兼容的通知 API,支持 Wear 和 Auto。 +- Permissions(权限):用于检查和请求应用权限的兼容性 API。 +- Preferences(偏好设置):提供了用户能够改变应用的功能和行为能力。 +- Sharing(共享):提供适合应用操作栏的共享操作。 +- Slices(切片):创建可在应用外部显示应用数据的灵活界面元素。 + +### UI(界面组件) +大多数的UI组件其实都包含在基础组件中的appcompat中,这里是一些独立组件库存在的UI组件,界面组件可提供各类view和辅助程序,让应用不仅简单易用,还能带来愉悦体验。它包含如下组件库: +- drawerlayout:抽屉布局 +- recyclerview:可复用的滑动列表 +- constraintlayout:约束布局 +- compose*: Jetpack compose声明式UI +- coordinatorlayout:顶层布局继承自Framelayout,可以实现子View之间的联动交互效果 +- swiperefreshlayout:下拉刷新布局 +- viewpager2:分页布局 +- Material Design Components * : MD组件 +- Animation & Transitions(动画&过度):提供各类内置动画,也可以自定义动画效果。 +- Emoji(表情符号):使用户在未更新系统版本的情况下也可以使用表情符号。 + - EmojiTextView + - EmojiEditTExt + - EmojiButton +- Fragment:组件化界面的基本单位。 +- Layout(布局):xml书写的界面布局或者使用Compose完成的界面。 + 用户界面结构(如应用程序的活动)由Layout定义。 它定义了View和ViewGroup对象。 可以通过两种方式创建View和ViewGroup:通过以XML声明UI元素或通过编写代码(即以编程方式)。 Jetpack的这一部分涵盖了一些最常见的布局,例如LinearLayout,RelativeLayout和全新的ConstraintLayout。 而且,官方的Jetpack布局文档提供了一些指导,以使用RecyclerView创建项目列表以及使用CardView创建卡布局。 用户可以看到一个视图。 EditView,TextView和Button是View的示例。 另一方面,ViewGroup是一个容器对象,它定义了View的布局结构,因此它是不可见的。 ViewGroup的示例是LinearLayout,RelativeLayout和ConstraintLayout。 +- Palette(调色板):从调色板中提取出有用的信息。 + + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/jetpack_compose.png?raw=true) + +[Android Jetpack Compose](https://developer.android.com/jetpack/compose)是2019 Google/IO大会上推出的一种声明式的UI开发框架,经过一年左右的演进,现在到了alpha阶段。Jetpack Compose是用于构建原生界面的新款Android工具包。它可简化并加快Android上的界面开发。使用更少的代码、强大的工具和直观的KotlinAPI,快速让应用生动而精彩,从此不再需要写xml,使用声明式的Compose函数来构建页面UI。 + +Compose由androidx中的6个Maven组ID构成。每个组都包含一套特定用途的功能,并各有专属的版本说明。下表介绍了各个组及指向其版本说明的链接: +- compose.animation:在Jetpack Compose应用中构建动画,丰富用户的体验。 +- compose.compiler:借助Kotlin编译器插件,转换@Composable functions(可组合函数)并启用优化功能。 +- compose.foundation:使用现成可用的构建块编写Jetpack Compose应用,还可扩展Foundation以构建您自己的设计系统元素。 +- compose.material:使用现成可用的Material Design组件构建Jetpack Compose UI。这是更高层级的Compose入口点,旨在提供与www.material.io上描述的组件一致的组件。 +- compose.runtime:Compose的编程模型和状态管理的基本构建块,以及Compose编译器插件针对的核心运行时。 +- compose.ui:与设备互动所需的Compose UI的基本组件,包括布局、绘图和输入。 +```kotlin +class MainActivity : AppCompatActivity() { + overridefun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + Text("Hello, Android技术杂货铺") + } + } +} +``` +废弃部分: +- asynclayoutinflater:异步生成UI +- localbroadcastmanager:本地广播 +- viewpager:使用vviewpager2 +- cardview:使用MaterialCardView替代 + + +## Jetpack的意义 +Jetpack里目前包含的内容,未来也会是Google大力维护和扩展的内容。对应开发者来说也是值得去学习使用的且相对无后顾之忧的。JetPack里没有的,除开一些优秀的第三方库,未来应该也会慢慢被新的API替代,逐渐边缘化,直至打上Deprecate注解。 + +以当下的环境来说,要开发出一个完全摆脱JetPack的APP是很难做到的。但是反过来讲JetPack也远远没有到成熟的地步,目前也还存在亟待解决的问题,未来可以做的事情还有很多。 + +关于使用的话,并不是所有库都建议使用,因为目前还有很多库在alpha版本。但是作为学习还是很有必要的,能给你日常的开发中多提供一些思路,这些是无可厚非的。 + + + + + +## Jetpack库列表及集成 + +[Jetpack各库版本及gradle集成使用](https://developer.android.com/jetpack/androidx/releases/activity) + + + +## 参考 +- [Jetpack Compose初体验 ](https://easyliu-ly.github.io/2020/12/12/android_jetpack/compose/) diff --git "a/ArchitectureComponents/1.\347\256\200\344\273\213(\344\270\200).md" "b/Jetpack/architecture/1.\347\256\200\344\273\213.md" similarity index 96% rename from "ArchitectureComponents/1.\347\256\200\344\273\213(\344\270\200).md" rename to "Jetpack/architecture/1.\347\256\200\344\273\213.md" index 7716d3b9..5345872d 100644 --- "a/ArchitectureComponents/1.\347\256\200\344\273\213(\344\270\200).md" +++ "b/Jetpack/architecture/1.\347\256\200\344\273\213.md" @@ -1,4 +1,4 @@ -1.简介(一) +1.简介 === 应用开发者面临的常见问题 @@ -97,6 +97,8 @@ override fun onDestroy() { 除了有可能引发内存泄漏的风险, 数据持久化也是一个经常困扰我们的问题.通常在屏幕旋转后,`UI`的对象都会被销毁重建,这将导致原来的对象数据不得不重新创建和获取,浪费资源的同时也会影响用户的体验. 通常的解决方法是,通过`SavedInstanceState`来存取数据,但`SavedInstanceState`存储的数据一般比较小,且数据对象还是必须重新构建. +为了将代码解耦以应对日益膨胀的代码量,工程师在应用程序中引入了“架构”的概念。使之在不影响应用程序各模块组件间通信的同时,还能够保持模块的相对独立。这样不仅有利于后期维护,也有利于代码测试。 + 上述两个问题可以通过使用`AAC`架构解决. diff --git "a/Jetpack/architecture/10.DataStore\347\256\200\344\273\213.md" "b/Jetpack/architecture/10.DataStore\347\256\200\344\273\213.md" new file mode 100644 index 00000000..2b6cdb21 --- /dev/null +++ "b/Jetpack/architecture/10.DataStore\347\256\200\344\273\213.md" @@ -0,0 +1,205 @@ +# 10.DataStore简介 + +Jetpack DataStore 是一种数据存储解决方案,允许您使用[协议缓冲区](https://developers.google.com/protocol-buffers)存储键值对或类型化对象。DataStore 使用 Kotlin 协程和 Flow 以异步、一致的事务方式存储数据。 + +如果您当前在使用 [`SharedPreferences`](https://developer.android.com/reference/kotlin/android/content/SharedPreferences) 存储数据,请考虑迁移到 DataStore。 + +**注意**:如果您需要支持大型或复杂数据集、部分更新或参照完整性,请考虑使用 [Room](https://developer.android.com/training/data-storage/room),而不是 DataStore。DataStore 非常适合简单的小型数据集,不支持部分更新或参照完整性。 + +## Preferences DataStore 和 Proto DataStore + +DataStore 提供两种不同的实现:Preferences DataStore 和 Proto DataStore。 + +- **Preferences DataStore** 像SharedPreferences一样,以键值对的形式进行基本类型的数据存储。DataStore 基于 Flow 实现异步存储,避免因为阻塞主线程带来的ANR问题 +- **Proto DataStore** 基于Protobuf实现任意自定义类型的数据存储,需要定义Protobuf的IDL,但是可以保证类型安全的访问 + + + +如需在您的应用中使用 Jetpack DataStore,请根据您要使用的实现向 Gradle 文件添加以下内容: + +```groovy +// Preferences DataStore (SharedPreferences like APIs) +dependencies { + implementation "androidx.datastore:datastore-preferences:1.0.0-beta01" + + // optional - RxJava2 support + implementation "androidx.datastore:datastore-preferences-rxjava2:1.0.0-beta01" + + // optional - RxJava3 support + implementation "androidx.datastore:datastore-preferences-rxjava3:1.0.0-beta01" +} +// Alternatively - use the following artifact without an Android dependency. +dependencies { + implementation "androidx.datastore:datastore-preferences-core:1.0.0-beta01" +} +``` + + + + + +Jetpack DataStore 是一种数据存储解决方案,允许您使用[协议缓冲区](https://developers.google.com/protocol-buffers)存储键值对或类型化对象。DataStore 使用 Kotlin 协程和 Flow 以异步、一致的事务方式存储数据。 + +如果您当前在使用 [`SharedPreferences`](https://developer.android.com/reference/kotlin/android/content/SharedPreferences) 存储数据,请考虑迁移到 DataStore。 + +**注意**:如果您需要支持大型或复杂数据集、部分更新或参照完整性,请考虑使用 [Room](https://developer.android.com/training/data-storage/room),而不是 DataStore。DataStore 非常适合简单的小型数据集,不支持部分更新或参照完整性。 + +## Preferences DataStore 和 Proto DataStore + +DataStore 提供两种不同的实现:Preferences DataStore 和 Proto DataStore。 + +- **Preferences DataStore** 使用键存储和访问数据。此实现不需要预定义的架构,也不确保类型安全。 +- **Proto DataStore** 将数据作为自定义数据类型的实例进行存储。此实现要求您使用[协议缓冲区](https://developers.google.com/protocol-buffers)来定义架构,但可以确保类型安全。 + +## 设置 + +如需在您的应用中使用 Jetpack DataStore,请根据您要使用的实现向 Gradle 文件添加以下内容: + +[类型化](https://developer.android.com/topic/libraries/architecture/datastore#类型化-datastore)[偏好设置](https://developer.android.com/topic/libraries/architecture/datastore#偏好设置-datastore) + +``` +// Typed DataStore (Typed API surface, such as Proto)dependencies { implementation "androidx.datastore:datastore:1.0.0-beta01" // optional - RxJava2 support implementation "androidx.datastore:datastore-rxjava2:1.0.0-beta01" // optional - RxJava3 support implementation "androidx.datastore:datastore-rxjava3:1.0.0-beta01"}// Alternatively - use the following artifact without an Android dependency.dependencies { implementation "androidx.datastore:datastore-core:1.0.0-beta01"} +``` + +**注意**:如果您将 `datastore-preferences-core` 工件与 Proguard 搭配使用,就必须手动将 Proguard 规则添加到 `proguard-rules.pro` 文件中,以免您的字段遭到删除。您可以点击[此处](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:datastore/datastore-preferences/proguard-rules.pro)查找必要的规则。 + +## 使用 Preferences DataStore 存储键值对 + +Preferences DataStore 实现使用 [`DataStore`](https://developer.android.com/reference/kotlin/androidx/datastore/core/DataStore) 和 [`Preferences`](https://developer.android.com/reference/kotlin/androidx/datastore/preferences/core/Preferences) 类将简单的键值对保留在磁盘上。 + +### 创建 Preferences DataStore + +使用由 [`preferencesDataStore`](https://developer.android.com/reference/kotlin/androidx/datastore/preferences/package-summary#dataStore) 创建的属性委托来创建 `Datastore` 实例。在您的 Kotlin 文件顶层调用该实例一次,便可在应用的所有其余部分通过此属性访问该实例。这样可以更轻松地将 `DataStore` 保留为单例。此外,如果您使用的是 RxJava,请使用 [`RxPreferenceDataStoreBuilder`](https://developer.android.com/reference/kotlin/androidx/datastore/rxjava2/RxDataStoreBuilder)。必需的 `name` 参数是 Preferences DataStore 的名称。 + +[Kotlin](https://developer.android.com/topic/libraries/architecture/datastore#kotlin)[Java](https://developer.android.com/topic/libraries/architecture/datastore#java) + +``` +// At the top level of your kotlin file:val Context.dataStore: DataStore by preferencesDataStore(name = "settings") +``` + +### 从 Preferences DataStore 读取内容 + +由于 Preferences DataStore 不使用预定义的架构,因此您必须使用相应的键类型函数为需要存储在 `DataStore` 实例中的每个值定义一个键。例如,如需为 int 值定义一个键,请使用 [`intPreferencesKey()`](https://developer.android.com/reference/kotlin/androidx/datastore/preferences/core/package-summary#intPreferencesKey(kotlin.String))。然后,使用 [`DataStore.data`](https://developer.android.com/reference/kotlin/androidx/datastore/core/DataStore#data) 属性,通过 `Flow` 提供适当的存储值。 + +[Kotlin](https://developer.android.com/topic/libraries/architecture/datastore#kotlin)[Java](https://developer.android.com/topic/libraries/architecture/datastore#java) + +``` +val EXAMPLE_COUNTER = intPreferencesKey("example_counter")val exampleCounterFlow: Flow = context.dataStore.data .map { preferences -> // No type safety. preferences[EXAMPLE_COUNTER] ?: 0} +``` + +### 将内容写入 Preferences DataStore + +Preferences DataStore 提供了一个 [`edit()`](https://developer.android.com/reference/kotlin/androidx/datastore/preferences/core/package-summary#edit) 函数,用于以事务方式更新 `DataStore` 中的数据。该函数的 `transform` 参数接受代码块,您可以在其中根据需要更新值。转换块中的所有代码均被视为单个事务。 + +[Kotlin](https://developer.android.com/topic/libraries/architecture/datastore#kotlin)[Java](https://developer.android.com/topic/libraries/architecture/datastore#java) + +``` +suspend fun incrementCounter() { context.dataStore.edit { settings -> val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0 settings[EXAMPLE_COUNTER] = currentCounterValue + 1 }} +``` + +## 使用 Proto DataStore 存储类型化的对象 + +Proto DataStore 实现使用 DataStore 和[协议缓冲区](https://developers.google.com/protocol-buffers)将类型化的对象保留在磁盘上。 + +### 定义架构 + +Proto DataStore 要求在 `app/src/main/proto/` 目录的 proto 文件中保存预定义的架构。此架构用于定义您在 Proto DataStore 中保存的对象的类型。如需详细了解如何定义 proto 架构,请参阅 [protobuf 语言指南](https://developers.google.com/protocol-buffers/docs/proto3)。 + +``` +syntax = "proto3"; option java_package = "com.example.application"; option java_multiple_files = true; message Settings { int32 example_counter = 1; } +``` + +**注意**:您的存储对象的类在编译时由 proto 文件中定义的 `message` 生成。请务必重新构建您的项目。 + +### 创建 Proto DataStore + +创建 Proto DataStore 来存储类型化对象涉及两个步骤: + +1. 定义一个实现 `Serializer` 的类,其中 `T` 是 proto 文件中定义的类型。此序列化器类会告知 DataStore 如何读取和写入您的数据类型。请务必为该序列化器添加默认值,以便在尚未创建任何文件时使用。 +2. 使用由 `dataStore` 创建的属性委托来创建 `DataStore` 的实例,其中 `T` 是在 proto 文件中定义的类型。在您的 Kotlin 文件顶层调用该实例一次,便可在应用的所有其余部分通过此属性委托访问该实例。`filename` 参数会告知 DataStore 使用哪个文件存储数据,而 `serializer` 参数会告知 DataStore 第 1 步中定义的序列化器类的名称。 + +[Kotlin](https://developer.android.com/topic/libraries/architecture/datastore#kotlin)[Java](https://developer.android.com/topic/libraries/architecture/datastore#java) + +``` +object SettingsSerializer : Serializer { override val defaultValue: Settings = Settings.getDefaultInstance() override suspend fun readFrom(input: InputStream): Settings { try { return Settings.parseFrom(input) } catch (exception: InvalidProtocolBufferException) { throw CorruptionException("Cannot read proto.", exception) } } override suspend fun writeTo( t: Settings, output: OutputStream) = t.writeTo(output)}val Context.settingsDataStore: DataStore by dataStore( fileName = "settings.pb", serializer = SettingsSerializer) +``` + +### 从 Proto DataStore 读取内容 + +使用 `DataStore.data` 显示所存储对象中相应属性的 `Flow`。 + +[Kotlin](https://developer.android.com/topic/libraries/architecture/datastore#kotlin)[Java](https://developer.android.com/topic/libraries/architecture/datastore#java) + +``` +val exampleCounterFlow: Flow = context.settingsDataStore.data .map { settings -> // The exampleCounter property is generated from the proto schema. settings.exampleCounter } +``` + +### 将内容写入 Proto DataStore + +Proto DataStore 提供了一个 [`updateData()`](https://developer.android.com/reference/kotlin/androidx/datastore/DataStore#updatedata) 函数,用于以事务方式更新存储的对象。`updateData()` 为您提供数据的当前状态,作为数据类型的一个实例,并在原子读-写-修改操作中以事务方式更新数据。 + +[Kotlin](https://developer.android.com/topic/libraries/architecture/datastore#kotlin)[Java](https://developer.android.com/topic/libraries/architecture/datastore#java) + +``` +suspend fun incrementCounter() { context.settingsDataStore.updateData { currentSettings -> currentSettings.toBuilder() .setExampleCounter(currentSettings.exampleCounter + 1) .build() }} +``` + +## 在同步代码中使用 DataStore + +**注意**:请尽可能避免在 DataStore 数据读取时阻塞线程。阻塞界面线程可能会导致 [ANR](https://developer.android.com/topic/performance/vitals/anr) 或界面卡顿,而阻塞其他线程可能会导致[死锁](https://en.wikipedia.org/wiki/Deadlock)。 + +DataStore 的主要优势之一是异步 API,但可能不一定始终能将周围的代码更改为异步代码。如果您使用的现有代码库采用同步磁盘 I/O,或者您的依赖项不提供异步 API,就可能出现这种情况。 + +Kotlin 协程提供 [`runBlocking()`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html) 协程构建器,以帮助消除同步与异步代码之间的差异。您可以使用 `runBlocking()` 从 DataStore 同步读取数据。RxJava 在 `Flowable` 上提供阻塞方法。以下代码会阻塞发起调用的线程,直到 DataStore 返回数据: + +[Kotlin](https://developer.android.com/topic/libraries/architecture/datastore#kotlin)[Java](https://developer.android.com/topic/libraries/architecture/datastore#java) + +``` +val exampleData = runBlocking { context.dataStore.data.first() } +``` + +对界面线程执行同步 I/O 操作可能会导致 ANR 或界面卡顿。您可以通过从 DataStore 异步预加载数据来减少这些问题: + +[Kotlin](https://developer.android.com/topic/libraries/architecture/datastore#kotlin)[Java](https://developer.android.com/topic/libraries/architecture/datastore#java) + +``` +override fun onCreate(savedInstanceState: Bundle?) { lifecycleScope.launch { context.dataStore.data.first() // You should also handle IOExceptions here. }} +``` + +这样,DataStore 可以异步读取数据并将其缓存在内存中。以后使用 `runBlocking()` 进行同步读取的速度可能会更快,或者如果初始读取已经完成,可能也可以完全避免磁盘 I/O 操作。 + + + + + + + + + + + +https://developer.android.com/topic/libraries/architecture/datastore#typed-datastore + +https://android-developers.googleblog.com/2020/09/prefer-storing-data-with-jetpack.html + + +https://blog.csdn.net/zzw0221/article/details/109274610 + +SharedPreferences 有着许多缺陷: 看起来可以在 UI 线程安全调用的同步 API 其实并不安全、没有提示错误的机制、缺少事务 API 等等。DataStore 是 SharedPreferences 的替代方案,它解决了 Shared Preferences 的绝大部分问题。DataStore 包含使用 Kotlin 协程和 Flow 实现的完全异步 API,可以处理数据迁移、保证数据一致性,并且可以处理数据损坏。 + +https://blog.csdn.net/weixin_42324979/article/details/112650189?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-3.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-3.control + + + +缺点: + +- 目前Jetpack Security没有支持DataStore,所以不能像SharedPreference一样支持加密 +- 不能安全的进行IPC,这点相对于SharedPreferences没有提升,有较强IPC需求的话首选MMKV +- 使用PB进行序列化时需要额外定义IDL,这会产生一定工作量 + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/Jetpack/architecture/11.Hilt\347\256\200\344\273\213.md" "b/Jetpack/architecture/11.Hilt\347\256\200\344\273\213.md" new file mode 100644 index 00000000..38ba0a7e --- /dev/null +++ "b/Jetpack/architecture/11.Hilt\347\256\200\344\273\213.md" @@ -0,0 +1,16 @@ +# 11.Hilt简介 + + + + + +https://zhuanlan.zhihu.com/p/335631378 + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! ` \ No newline at end of file diff --git "a/Jetpack/architecture/12.Navigation\347\256\200\344\273\213.md" "b/Jetpack/architecture/12.Navigation\347\256\200\344\273\213.md" new file mode 100644 index 00000000..18a39661 --- /dev/null +++ "b/Jetpack/architecture/12.Navigation\347\256\200\344\273\213.md" @@ -0,0 +1,487 @@ +# 12.Navigation简介 + +单个Activity嵌套多个Fragment的UI架构模式,已经被大多数Android工程师所接受和采用。但是,对Fragment的管理一直是一件比较麻烦的事情。工程师需要通过FragmentManager和FragmentTransaction来管理Fragment之间的切换。页面的切换通常还包括对应用程序App bar的管理、Fragment间的切换动画,以及Fragment间的参数传递。纯代码的方式使用起来不是特别友好,并且Fragment和App bar在管理和使用的过程中显得很混乱。 + +为此,Jetpack提供了一个名为Navigation的组件,旨在方便我们管理页面和App bar。 + +它具有以下优势: + +- 可视化的页面导航图,类似于Apple Xcode中的StoryBoard,便于我们理清页面间的关系。 +- 通过destination和action完成页面间的导航。 +- 方便添加页面切换动画。 +- 页面间类型安全的参数传递。 +- 通过NavigationUI类,对菜单、底部导航、抽屉菜单导航进行统一的管理。 +- 支持深层链接DeepLink。 + +Navigation 组件旨在用于具有一个主 Activity 和多个 Fragment 目的地的应用。主 Activity 与导航图相关联,且包含一个负责根据需要交换目的地的 `NavHostFragment`。在具有多个 Activity 目的地的应用中,每个 Activity 均拥有其自己的导航图。 + +## 依赖 + +如果想要使用Navigation,需要现在build.gradle文件中添加以下依赖: + +``` +dependencies { + def nav_version = "2.3.5" + + // Java language implementation + implementation "androidx.navigation:navigation-fragment:$nav_version" + implementation "androidx.navigation:navigation-ui:$nav_version" + + // Kotlin + implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" + implementation "androidx.navigation:navigation-ui-ktx:$nav_version" + + // Feature module Support + implementation "androidx.navigation:navigation-dynamic-features-fragment:$nav_version" + + // Testing Navigation + androidTestImplementation "androidx.navigation:navigation-testing:$nav_version" + + // Jetpack Compose Integration + implementation "androidx.navigation:navigation-compose:1.0.0-alpha10" +} + +``` + + + + + +## Navigation的主要元素 + +导航组件由以下三个关键部分组成: + +1. Navigation Graph + + 在一个集中位置包含所有导航相关信息的 XML 资源。这包括应用内所有单个内容区域(称为*目标*)以及用户可以通过应用获取的可能路径。 + +2. NavHost + + 显示导航图中目标的空白容器。导航组件包含一个默认NavHost实现 (NavHostFragment),可显示Fragment目标。 + +3. NavController + + 在NavHost中管理应用导航的对象。当用户在整个应用中移动时,NavController会安排NavHost中目标内容的交换。 + +在应用中导航时,您告诉NavController,您想沿导航图中的特定路径导航至特定目标,或直接导航至特定目标。NavController便会在NavHost中显示相应目标。 + + + +## Navigation Graph(导航图) + +导航图是一种资源文件,其中包含您的所有目的地和操作。该图表会显示应用的所有导航路径。 + + + +如需向项目添加导航图,请执行以下操作: + +1. 在“Project”窗口中,右键点击 `res` 目录,然后依次选择 **New > Android Resource File**。此时系统会显示 **New Resource File** 对话框。 +2. 在 **File name** 字段中输入名称,例如“nav_graph”。 +3. 从 **Resource type** 下拉列表中选择 **Navigation**,然后点击 **OK**。 + +``` + + + +``` + +`` 元素是导航图的根元素。当您向图表添加目的地和连接操作时,可以看到相应的 `` 和 `` 元素在此处显示为子元素。如果您有[嵌套图表](https://developer.android.com/guide/navigation/navigation-nested-graphs),它们将显示为子 `` 元素。 + +## 向 Activity 添加 NavHost + +导航宿主是 Navigation 组件的核心部分之一。导航宿主是一个空容器,用户在您的应用中导航时,目的地会在该容器中交换进出。 + +导航宿主必须派生于 [`NavHost`](https://developer.android.com/reference/androidx/navigation/NavHost)。Navigation 组件的默认 `NavHost` 实现 ([`NavHostFragment`](https://developer.android.com/reference/androidx/navigation/fragment/NavHostFragment)) 负责处理 Fragment 目的地的交换。 + + + +### 通过 XML 添加 NavHostFragment + +以下 XML 示例显示了作为应用主 Activity 一部分的 `NavHostFragment`: + +```xml + + + + + + + + + + +``` + +NavHostFragment是一个特殊的Fragment,我们需要将其添加到Activity的布局文件中,作为其他Fragment的容器。 + +请注意以下几点: + +- `android:name` 属性包含 `NavHost` 实现的类名称。 +- `app:navGraph` 属性将 `NavHostFragment` 与导航图相关联。导航图会在此 `NavHostFragment` 中指定用户可以导航到的所有目的地。 +- `app:defaultNavHost="true"` 属性确保您的 `NavHostFragment` 会自动处理系统返回键,即当用户按下手机的返回按钮时,系统能自动将当前所展示的Fragment退出。请注意,只能有一个默认 `NavHost`。如果同一布局(例如,双窗格布局)中有多个宿主,请务必仅指定一个默认 `NavHost`。 + + + +```xml + + // 起始fragment + + +``` + +### Action + +```xml + + + + + + + + +``` + +在导航图中,操作由 `` 元素表示。操作至少应包含自己的 ID 和用户应转到的目的地的 ID。 + +## 导航到目的地 + +导航到目的地是使用 [`NavController`](https://developer.android.com/reference/androidx/navigation/NavController) 完成的,它是一个在 `NavHost` 中管理应用导航的对象。每个 `NavHost` 均有自己的相应 `NavController`。您可以使用以下方法之一检索 `NavController`: + +**Kotlin**: + +- [`Fragment.findNavController()`](https://developer.android.com/reference/kotlin/androidx/navigation/fragment/package-summary#findnavcontroller) +- [`View.findNavController()`](https://developer.android.com/reference/kotlin/androidx/navigation/package-summary#(android.view.View).findNavController()) +- [`Activity.findNavController(viewId: Int)`](https://developer.android.com/reference/kotlin/androidx/navigation/package-summary#findnavcontroller) + +**Java**: + +- [`NavHostFragment.findNavController(Fragment)`](https://developer.android.com/reference/androidx/navigation/fragment/NavHostFragment#findNavController(android.support.v4.app.Fragment)) +- [`Navigation.findNavController(Activity, @IdRes int viewId)`](https://developer.android.com/reference/androidx/navigation/Navigation#findNavController(android.app.Activity, int)) +- [`Navigation.findNavController(View)`](https://developer.android.com/reference/androidx/navigation/Navigation#findNavController(android.view.View)) + +使用 `FragmentContainerView` 创建 `NavHostFragment`,或通过 `FragmentTransaction` 手动将 `NavHostFragment` 添加到您的 Activity 时,尝试通过 `Navigation.findNavController(Activity, @IdRes int)` 检索 Activity 的 `onCreate()` 中的 `NavController` 将失败。您应改为直接从 `NavHostFragment` 检索 `NavController`。 + +```kotlin +val navHostFragment = + supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment +val navController = navHostFragment.navController +navController.navigate(R.id.action_blankFragment_to_blankFragment2) +``` + +对于按钮,您还可以使用 [`Navigation`](https://developer.android.com/reference/androidx/navigation/Navigation) 类的 [`createNavigateOnClickListener()`](https://developer.android.com/reference/androidx/navigation/Navigation#createNavigateOnClickListener(int)) 便捷方法导航到目的地,如下例所示: + +```kotlin +button.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.next_fragment, null)) + +``` + + + +## 使用 DeepLinkRequest 导航 + +您可以使用 [`navigate(NavDeepLinkRequest)`](https://developer.android.com/reference/androidx/navigation/NavController#navigate(androidx.navigation.NavDeepLinkRequest)) 直接导航到[隐式深层链接目的地](https://developer.android.com/guide/navigation/navigation-deep-link#implicit),如下例所示: + +```kotlin +val request = NavDeepLinkRequest.Builder + .fromUri("android-app://androidx.navigation.app/profile".toUri()) + .build() +findNavController().navigate(request) + +``` + +## 导航和返回堆栈 + +Android 会维护一个[返回堆栈](https://developer.android.com/guide/components/activities/tasks-and-back-stack),其中包含您之前访问过的目的地。当用户打开您的应用时,应用的第一个目的地就放置在堆栈中。每次调用 [`navigate()`](https://developer.android.com/reference/androidx/navigation/NavController#navigate(int)) 方法都会将另一目的地放置到堆栈的顶部。点按**向上**或**返回**会分别调用 [`NavController.navigateUp()`](https://developer.android.com/reference/androidx/navigation/NavController#navigateUp()) 和 [`NavController.popBackStack()`](https://developer.android.com/reference/androidx/navigation/NavController#popBackStack()) 方法,用于移除(或弹出)堆栈顶部的目的地。 + +`NavController.popBackStack()` 会返回一个布尔值,表明它是否已成功返回到另一个目的地。当返回 `false` 时,最常见的情况是手动弹出图的起始目的地。 + +如果该方法返回 `false`,则 `NavController.getCurrentDestination()` 会返回 `null`。您应负责导航到新目的地,或通过对 Activity 调用 `finish()` 来处理弹出情况,如下例所示: + +```kotlin + +if (!navController.popBackStack()) { + // Call finish() on your Activity + finish() +} + +``` + +## popUpTo 和 popUpToInclusive + +使用操作进行导航时,您可以选择从返回堆栈上弹出其他目的地。例如,如果您的应用具有初始登录流程,那么在用户登录后,您应将所有与登录相关的目的地从返回堆栈上弹出,这样返回按钮就不会将用户带回登录流程。 + +如需在从一个目的地导航到另一个目的地时弹出目的地,请在关联的 `` 元素中添加 `app:popUpTo` 属性。`app:popUpTo` 会告知 Navigation 库在调用 `navigate()` 的过程中从返回堆栈上弹出一些目的地。属性值是应保留在堆栈中的最新目的地的 ID。 + +您还可以添加 `app:popUpToInclusive="true"`,以表明在 `app:popUpTo` 中指定的目的地也应从返回堆栈中移除。 + +## 通过 引用其他导航图 + +在导航图中,您可以使用 `include` 引用其他图。虽然这在功能上与使用嵌套图相同,但 `include` 可让您使用其他项目模块或库项目中的图,如以下示例所示: + +```xml + + + + + + + + + + + ... + +``` + +```xml + + + + + + +``` + + + +## 创建全局操作 + +您可以使用全局操作来创建可由多个目的地共用的通用操作。例如,您可能想要不同目的地中的多个按钮导航到同一应用主屏幕。 + +```xml + + + + ... + + + + +``` + +如需在代码中使用某个全局操作,请将该全局操作的资源 ID 传递到每个界面元素的 [`navigate()`](https://developer.android.com/reference/androidx/navigation/NavController#navigate(int)) 方法,如以下示例所示: + +```kotlin +viewTransactionButton.setOnClickListener { view -> + view.findNavController().navigate(R.id.action_global_mainFragment) +} +``` + +## 使用 Safe Args 实现类型安全的导航 + +如需在目的地之间导航,建议使用 Safe Args Gradle 插件。此插件可生成简单的对象和构建器类,以便在目的地之间实现类型安全的导航。我们强烈建议您在导航以及[在目的地之间传递数据](https://developer.android.com/guide/navigation/navigation-pass-data#Safe-args)时使用 Safe Args。 + + + +如需将 [Safe Args](https://developer.android.com/topic/libraries/architecture/navigation/navigation-pass-data#Safe-args) 添加到您的项目中,请在顶层 `build.gradle` 文件中包含以下 `classpath`: + +```xml +buildscript { + repositories { + google() + } + dependencies { + def nav_version = "2.3.5" + classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" + } +} +``` + +您还必须应用以下两个可用插件之一。 + +如需生成适用于 Java 模块或 Java 和 Kotlin 混合模块的 Java 语言代码,请将以下行添加到**应用或模块**的 `build.gradle` 文件中: + +`apply plugin: "androidx.navigation.safeargs"` + +此外,如需生成适用于 Kotlin 独有的模块的 Kotlin 代码,请添加以下行: + +`apply plugin: "androidx.navigation.safeargs.kotlin"` + +根据[迁移到 AndroidX](https://developer.android.com/jetpack/androidx/migrate#migrate)) 文档,您的 [`gradle.properties` 文件](https://developer.android.com/studio/build#properties-files)中必须具有 `android.useAndroidX=true`。 + +启用 Safe Args 后,生成的代码会包含已定义的每个操作的类和方法,以及与每个发送目的地和接收目的地相对应的类。 + +Safe Args 为生成操作的每个目的地生成一个类。生成的类名称会在源目的地类名称的基础上添加“Directions”。例如,如果源目的地的名称为 `SpecifyAmountFragment`,则生成的类的名称为 `SpecifyAmountFragmentDirections`。 + +生成的类为源目的地中定义的每个操作提供了一个静态方法。该方法接受任何定义的[操作参数](https://developer.android.com/guide/navigation/navigation-pass-data)为参数,并返回可直接传递到 [`navigate()`](https://developer.android.com/reference/androidx/navigation/NavController.html?skip_cache=true#navigate(androidx.navigation.NavDirections)) 的 [`NavDirections`](https://developer.android.com/reference/androidx/navigation/NavDirections.html?skip_cache=true) 对象。 + + + +### Safe Args 示例 + +例如,假设我们的导航图包含一个操作,该操作将两个目的地 `SpecifyAmountFragment` 和 `ConfirmationFragment` 连接起来。`ConfirmationFragment` 接受您作为操作的一部分提供的单个 `float` 参数。 + +Safe Args 会生成一个 `SpecifyAmountFragmentDirections` 类,其中只包含一个 `actionSpecifyAmountFragmentToConfirmationFragment()` 方法和一个名为 `ActionSpecifyAmountFragmentToConfirmationFragment` 的内部类。这个内部类派生自 `NavDirections` 并存储了关联的操作 ID 和 `float` 参数。然后,您可以将返回的 `NavDirections` 对象直接传递到 `navigate()`,如下例所示: + +```kotlin +override fun onClick(v: View) { + val amount: Float = ... + val action = + SpecifyAmountFragmentDirections + .actionSpecifyAmountFragmentToConfirmationFragment(amount) + v.findNavController().navigate(action) +} +``` + +## 传递参数 + +Navigation 支持您通过定义目的地参数将数据附加到导航操作。例如,用户个人资料目的地可能会根据用户 ID 参数来确定要显示哪个用户。 + +通常情况下,强烈建议您仅在目的地之间传递最少量的数据。例如,您应该传递键来检索对象而不是传递对象本身,因为在 Android 上用于保存所有状态的总空间是有限的。如果您需要传递大量数据,不妨考虑使用 [`ViewModel`](https://developer.android.com/reference/androidx/lifecycle/ViewModel)(如[在 Fragment 之间共享数据](https://developer.android.com/topic/libraries/architecture/viewmodel#sharing)中所述)。 + +```xml + + + +``` + +通过声明argement节点来指定参数。 + +启用 Safe Args 后,生成的代码会为每个操作包含以下类型安全的类和方法,以及每个发送和接收目的地。 + +- 为生成操作的每一个目的地创建一个类。该类的名称是在源目的地的名称后面加上“Directions”。例如,如果源目的地是名为 `SpecifyAmountFragment` 的 Fragment,则生成的类的名称为 `SpecifyAmountFragmentDirections`。 + + 该类会为源目的地中定义的每个操作提供一个方法。 + +- 对于用于传递参数的每个操作,都会创建一个 inner 类,该类的名称根据操作的名称确定。例如,如果操作名称为 `confirmationAction,`,则类名称为 `ConfirmationAction`。如果您的操作包含不带 `defaultValue` 的参数,则您可以使用关联的 action 类来设置参数值。 + +- 为接收目的地创建一个类。该类的名称是在目的地的名称后面加上“Args”。例如,如果目的地 Fragment 的名称为 `ConfirmationFragment,`,则生成的类的名称为 `ConfirmationFragmentArgs`。可以使用该类的 `fromBundle()` 方法检索参数。 + +``` +override fun onClick(v: View) { val amountTv: EditText = view!!.findViewById(R.id.editTextAmount) val amount = amountTv.text.toString().toInt() val action = SpecifyAmountFragmentDirections.confirmationAction(amount) v.findNavController().navigate(action)} +``` + +在接收目的地的代码中,请使用 [`getArguments()`](https://developer.android.com/reference/androidx/fragment/app/Fragment#getArguments()) 方法来检索 bundle 并使用其内容。使用 `-ktx` 依赖项时,Kotlin 用户还可以使用 `by navArgs()` 属性委托来访问参数。 + +```kotlin +val args: ConfirmationFragmentArgs by navArgs() + +override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val tv: TextView = view.findViewById(R.id.textViewAmount) + val amount = args.amount + tv.text = amount.toString() +} +``` + +## 使用 Bundle 对象在目的地之间传递参数 + +如果您不使用 Gradle,仍然可以使用 `Bundle` 对象在目的地之间传递参数。创建 `Bundle` 对象并使用 [`navigate()`](https://developer.android.com/reference/androidx/navigation/NavController#navigate(int)) 将它传递给目的地,如下所示: + +``` +val bundle = bundleOf("amount" to amount)view.findNavController().navigate(R.id.confirmationAction, bundle) +``` + +在接收目的地的代码中,请使用 [`getArguments()`](https://developer.android.com/reference/androidx/fragment/app/Fragment#getArguments()) 方法来检索 `Bundle` 并使用其内容: + +```kotlin +val tv = view.findViewById(R.id.textViewAmount) +tv.text = arguments?.getString("amount") +``` + + + +## NavigationUI + +导航图是Navigation组件中很重要的一部分,它可以帮助我们快速了解页面之间的关系,再通过NavController便可以完成页面的切换工作。而在页面的切换过程中,通常还伴随着App bar中menu菜单的变化。对于不同的页面,App bar中的menu菜单很可能是不一样的。App bar中的各种按钮和菜单,同样承担着页面切换的工作。例如,当ActionBar左边的返回按钮被单击时,我们需要响应该事件,返回到上一个页面。既然Navigation和App bar都需要处理页面切换事件,那么,为了方便管理,Jetpack引入了NavigationUI组件,使App bar中的按钮和菜单能够与导航图中的页面关联起来。 + +`NavigationUI` 支持以下顶部应用栏类型: + +- [`Toolbar`](https://developer.android.com/reference/android/widget/Toolbar) +- [`CollapsingToolbarLayout`](https://developer.android.com/reference/com/google/android/material/appbar/CollapsingToolbarLayout) +- [`ActionBar`](https://developer.android.com/reference/androidx/appcompat/app/ActionBar) + +```kotlin +override fun onCreate(savedInstanceState: Bundle?) { + ... + + val navHostFragment = + supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment + val navController = navHostFragment.navController + val appBarConfiguration = AppBarConfiguration( + topLevelDestinationIds = setOf(), + fallbackOnNavigateUpListener = ::onSupportNavigateUp + ) + findViewById(R.id.toolbar) + .setupWithNavController(navController, appBarConfiguration) +} + +``` + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git a/Jetpack/architecture/13.Jetpack MVVM.md b/Jetpack/architecture/13.Jetpack MVVM.md new file mode 100644 index 00000000..0ed02013 --- /dev/null +++ b/Jetpack/architecture/13.Jetpack MVVM.md @@ -0,0 +1,47 @@ +# 13.Jetpack MVVM + +项目地址:[android-architecture](https://github.com/googlesamples/android-architecture) +`Google`将该项目命名为`Android`的架构蓝图,我想从名字上已可以看穿一切。 + +在它的官方介绍中是这样说的: + +> The Android framework offers a lot of flexibility when it comes to defining how to organize and architect an Android app. This freedom, whilst very valuable, can also result in apps with large classes, inconsistent naming and architectures (or lack of) that can make testing, maintaining and extending difficult. + +> Android Architecture Blueprints is meant to demonstrate possible ways to help with these common problems. In this project we offer the same application implemented using different architectural concepts and tools. + +> You can use these samples as a reference or as a starting point for creating your own apps. The focus here is on code structure, architecture, testing and maintainability. However, bear in mind that there are many ways to build apps with these architectures and tools, depending on your priorities, so these shouldn't be considered canonical examples. The UI is deliberately kept simple. + +Jetpack MVVM 是 MVVM 模式在 Android 开发中的一个具体实现,是 Android中 Google 官方提供并推荐的 MVVM实现方式。 +不仅通过数据驱动完成彻底解耦,还兼顾了 Android 页面开发中其他不可预期的错误,例如Lifecycle 能在妥善处理 页面生命周期 避免view空指针问题,ViewModel使得UI发生重建时 无需重新向后台请求数据,节省了开销,让视图重建时更快展示数据。 +首先,请查看下图,该图显示了所有模块应如何彼此交互: + +各模块对应MVVM架构: + +View层:Activity/Fragment +ViewModel层:Jetpack ViewModel + Jetpack LivaData +Model层:Repository仓库,包含 本地持久性数据 和 服务端数据 + +View层 包含了我们平时写的Activity/Fragment/布局文件等与界面相关的东西。 +ViewModel层 用于持有和UI元素相关的数据,以保证这些数据在屏幕旋转时不会丢失,并且还要提供接口给View层调用以及和仓库层进行通信。 +仓库层 要做的主要工作是判断调用方请求的数据应该是从本地数据源中获取还是从网络数据源中获取,并将获取到的数据返回给调用方。本地数据源可以使用数据库、SharedPreferences等持久化技术来实现,而网络数据源则通常使用Retrofit访问服务器提供的Webservice接口来实现。 +另外,图中所有的箭头都是单向的,例如View层指向了ViewModel层,表示View层会持有ViewModel层的引用,但是反过来ViewModel层却不能持有View层的引用。除此之外,引用也不能跨层持有,比如View层不能持有仓库层的引用,谨记每一层的组件都只能与它相邻层的组件进行交互。 +这种设计打造了一致且愉快的用户体验。无论用户上次使用应用是在几分钟前还是几天之前,现在回到应用时都会立即看到应用在本地保留的数据。如果此数据已过期,则应用的Repository将开始在后台更新数据。 + +有人可能会有疑惑:怎么完全没有提 DataBinding、双向绑定? +实际上,这也是我之前的疑惑。 没有提 是因为: + +我不想让读者 一提到 MVVM 就和DataBinding联系起来 +我想让读者 抓住 MVVM 数据驱动 的本质。 +而DataBinding提供的双向绑定,是用来完善Jetpack MVVM 的工具,其本身在业界又非常具有争议性。 +掌握本篇内容,已经是Google推荐的开发架构,就已经实现 MVVM 模式。在Google官方的 应用架构指南 中 也同样丝毫没有提到 DataBinding。 + + +## 参考 +- [“终于懂了“系列:Jetpack AAC完整解析(四)MVVM - Android架构探索!](https://juejin.cn/post/6921321173661777933) + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! ` \ No newline at end of file diff --git "a/Jetpack/architecture/14.findViewById\347\232\204\350\277\207\345\216\273\345\217\212\346\234\252\346\235\245.md" "b/Jetpack/architecture/14.findViewById\347\232\204\350\277\207\345\216\273\345\217\212\346\234\252\346\235\245.md" new file mode 100644 index 00000000..0fc2bd0b --- /dev/null +++ "b/Jetpack/architecture/14.findViewById\347\232\204\350\277\207\345\216\273\345\217\212\346\234\252\346\235\245.md" @@ -0,0 +1,22 @@ +# 14.findViewById的过去及未来 + +We have lots of alternatives for this, and you may wonder why do we need another solution. Let’s compare the different solutions based on these criteria: null-safety, compile-time safety, and speed. + +| Column 1 | **[ButterKnife](https://github.com/JakeWharton/butterknife)** | [**Kotlin Synthetics**](https://developer.android.com/kotlin/ktx) | [**Data Binding**](https://developer.android.com/topic/libraries/data-binding) | [**findViewById**](https://developer.android.com/reference/android/app/Activity#findViewById(int)) | [View Binding](https://developer.android.com/topic/libraries/view-binding) | +| --------------------- | :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | +| **Fast** | ❌ * | ✅ | ❌ * | ✅ | ✅ | +| **Null-safe** | ❌ | ❌ | ✅ | ❌ | ✅ | +| **Compile-time safe** | ❌ | ❌ | ✅ | ✅ ** | ✅ | + +\* ButterKnife and Data Binding solutions are slower because they use an annotation-based approach + ** `findViewById()` is compile-time safe since API 26 because we don’t need to cast the type of view anymore. + +https://juejin.cn/post/6905942568467759111 + +https://medium.com/mobile-app-development-publication/how-android-access-view-item-the-past-to-the-future-bb003ae84527 + + + +## 参考 +- [Kotlin 插件的落幕,ViewBinding 的崛起](https://juejin.cn/post/6905942568467759111) +- [How Android Access View Item: The Past to the Future](https://medium.com/mobile-app-development-publication/how-android-access-view-item-the-past-to-the-future-bb003ae84527) \ No newline at end of file diff --git "a/Jetpack/architecture/2.ViewBinding\347\256\200\344\273\213.md" "b/Jetpack/architecture/2.ViewBinding\347\256\200\344\273\213.md" new file mode 100644 index 00000000..43f23558 --- /dev/null +++ "b/Jetpack/architecture/2.ViewBinding\347\256\200\344\273\213.md" @@ -0,0 +1,400 @@ +# 2.ViewBinding简介 + +ViewBinding是Google在2019年I/O大会上公布的一款Android视图绑定工具,在Android Studio 3.6中添加的一个新功能,更准确的说,它是DataBinding的一个更轻量变体,为什么要使用View Binding呢?答案是性能。许多开发者使用Data Binding库来引用Layout XML中的视图,而忽略它的其他强大功能。相比来说,自动生成代码ViewBinding其实比DataBinding性能更好。但是传统的方式使用View Binding却不是很好,因为会有很多样板代码(垃圾代码)。 + +通过ViewBinding,你可以更轻松的编写可与视图交互的代码。在模块中启用视图绑定之后,系统会为该模块中的每个XML布局文件生成一个绑定类。绑定类的实例包含对在相应布局中具有ID的所有视图的直接引用。在大多数情况下,视图绑定会替代findViewById。 + +## 使用方法 + +### 1.build.gradle中开启 +在build.gradle文件中的androidj节点添加如下代码: +``` +android { + ... + buildFeatures { + viewBinding true + } +} +``` +重新编译后系统会为每个布局文件生成对应的Binding类,该类中包含对应布局中具有id的所有视图的直接饮用。生成类的目录在app/build/generated/data_binding_base_class_source_out中。 +如果项目中存在多个模块,则需要在每个模块的build.gradle文件中都加上该配置。 +假设某个布局文件的名称为result_profile.xml: + +```xml + + + +