[OpenGL ES 02]OpenGL ES渲染管线与着色器
罗朝辉 (http://blog.csdn.net/kesalin)
在前文《[OpenGL ES 01]OpenGL ES之初体验》中我们学习了如何在 iOS 平台上设置OpenGL ES 环境,主要是设置 CAEAGLLayer 属性,创建 EAGLContext,创建和使用 renderbuffer 和 framebuffer,并知道如何清屏。但实际上并没有真正描绘点什么。在本文中,我们将学习OpenGL ES 渲染管线,顶点着色器和片元着色器相关知识,然后使用可编程管线在屏幕上描绘一个简单三角形。
一,渲染管线在 OpenGL ES 1.0 版本中,支持固定管线,而 OpenGL ES 2.0 版本不再支持固定管线,只支持可编程管线。什么是管线?什么又是固定管线和可编程管线?管线(pipeline)也称渲染管线,因为 OpenGL ES在渲染处理过程中会顺序执行一系列操作,这一系列相关的处理阶段就被称为OpenGL ES 渲染管线。pipeline 来源于福特汽车生产车间的流水线作业,在OpenGL ES 渲染过程中也是一样,一个操作接着一个操作进行,就如流水线作业一样,这样的实现极大地提供了渲染的效率。整个渲染管线如下图所示:
图中阴影部分的 Vertex Shader 和 Fragment Shader 是可编程管线。可编程管线就是说这个操作可以动态编程实现而不必固定写死在代码中。可动态编程实现这一功能一般都是脚本提供的,在OpenGL ES 中也一样,编写这样脚本的能力是由着色语言(Shader Language)提供的。那可编程管线有什么好处呢?方便我们动态修改渲染过程,而无需重写编译代码,当然也和很多脚本语言一样,调试起来不太方便。
再回到上图,这张图就是 OpenGL ES 的“架构图”,学习OpenGL ES 就是学习这张图中的每一个部分,在这里先粗略地介绍一下。
Vertex Array/Buffer objects:顶点数据来源,这时渲染管线的顶点输入,通常使用 Buffer objects效率更好。在今天的示例中,简单起见,使用的是 Vertex Array;
Vertex Shader:顶点着色器通过可编程的方式实现对顶点的操作,如进行坐标空间转换,计算 per-vertex color以及纹理坐标;
Primitive Assembly:图元装配,经过着色器处理之后的顶点在图片装配阶段被装配为基本图元。OpenGL ES 支持三种基本图元:点,线和三角形,它们是可被 OpenGL ES 渲染的。接着对装配好的图元进行裁剪(clip):保留完全在视锥体中的图元,丢弃完全不在视锥体中的图元,对一半在一半不在的图元进行裁剪;接着再对在视锥体中的图元进行剔除处理(cull):这个过程可编码来决定是剔除正面,背面还是全部剔除。
Rasterization:光栅化。在光栅化阶段,基本图元被转换为二维的片元(fragment),fragment 表示可以被渲染到屏幕上的像素,它包含位置,颜色,纹理坐标等信息,这些值是由图元的顶点信息进行插值计算得到的。这些片元接着被送到片元着色器中处理。这是从顶点数据到可渲染在显示设备上的像素的质变过程。
Fragment Shader:片元着色器通过可编程的方式实现对片元的操作。在这一阶段它接受光栅化处理之后的fragment,color,深度值,模版值作为输入。
Per-Fragment Operation:在这一阶段对片元着色器输出的每一个片元进行一系列测试与处理,从而决定最终用于渲染的像素。这一系列处理过程如下:
Pixel ownership test:该测试决定像素在 framebuffer 中的位置是不是为当前 OpenGL ES 所有。也就是说测试某个像素是否对用户可见或者被重叠窗口所阻挡;
Scissor Test:剪裁测试,判断像素是否在由 glScissor 定义的剪裁矩形内,不在该剪裁区域内的像素就会被剪裁掉;
Stencil Test:模版测试,将模版缓存中的值与一个参考值进行比较,从而进行相应的处理;
Depth Test:深度测试,比较下一个片段与帧缓冲区中的片段的深度,从而决定哪一个像素在前面,哪一个像素被遮挡;
Blending:混合,混合是将片段的颜色和帧缓冲区中已有的颜色值进行混合,并将混合所得的新值写入帧缓冲;
Dithering:抖动,抖动是使用有限的色彩让你看到比实际图象更多色彩的显示方式,以缓解表示颜色的值的精度不够大而导致的颜色剧变的问题。
Framebuffer:这是流水线的最后一个阶段,Framebuffer 中存储这可以用于渲染到屏幕或纹理中的像素值,也可以从Framebuffer 中读回像素值,但不能读取其他值(如深度值,模版值等)。
二,顶点着色器下面来仔细看看顶点着色器:
顶点着色器接收的输入:
Attributes:由 vertext array 提供的顶点数据,如空间位置,法向量,纹理坐标以及顶点颜色,它是针对每一个顶点的数据。属性只在顶点着色器中才有,片元着色器中没有属性。属性可以理解为针对每一个顶点的输入数据。OpenGL ES 2.0 规定了所有实现应该支持的最大属性个数不能少于 8 个。
Uniforms:uniforms保存由应用程序传递给着色器的只读常量数据。在顶点着色器中,这些数据通常是变换矩阵,光照参数,颜色等。由 uniform 修饰符修饰的变量属于全局变量,该全局性对顶点着色器与片元着色器均可见,也就是说,这两个着色器如果被连接到同一个应用程序中,它们共享同一份 uniform 全局变量集。因此如果在这两个着色器中都声明了同名的 uniform 变量,要保证这对同名变量完全相同:同名+同类型,因为它们实际是同一个变量。此外,uniform 变量存储在常量存储区,因此限制了 uniform 变量的个数,OpenGL ES 2.0 也规定了所有实现应该支持的最大顶点着色器 uniform 变量个数不能少于 128 个,最大的片元着色器 uniform 变量个数不能少于 16 个。
Samplers:一种特殊的 uniform,用于呈现纹理。sampler 可用于顶点着色器和片元着色器。
Shader program:由 main 申明的一段程序源码,描述在顶点上执行的操作:如坐标变换,计算光照公式来产生 per-vertex 颜色或计算纹理坐标。
顶点着色器的输出:
Varying:varying 变量用于存储顶点着色器的输出数据,当然也存储片元着色器的输入数据,varying 变量最终会在光栅化处理阶段被线性插值。顶点着色器如果声明了 varying 变量,它必须被传递到片元着色器中才能进一步传递到下一阶段,因此顶点着色器中声明的 varying 变量都应在片元着色器中重新声明同名同类型的 varying 变量。OpenGL ES 2.0 也规定了所有实现应该支持的最大 varying 变量个数不能少于 8 个。
在顶点着色器阶段至少应输出位置信息-即内建变量:gl_Position,其它两个可选的变量为:gl_FrontFacing 和 gl_PointSize。
三,片元着色器接下来仔细看看片元着色器:
片元管理器接受如下输入:
Varyings:这个在前面已经讲过了,顶点着色器阶段输出的 varying 变量在光栅化阶段被线性插值计算之后输出到片元着色器中作为它的输入,即上图中的 gl_FragCoord,gl_FrontFacing 和 gl_PointCoord。OpenGL ES 2.0 也规定了所有实现应该支持的最大 varying 变量个数不能少于 8 个。
Uniforms:前面也已经讲过,这里是用于片元着色器的常量,如雾化参数,纹理参数等;OpenGL ES 2.0 也规定了所有实现应该支持的最大的片元着色器 uniform 变量个数不能少于 16 个。
Samples:一种特殊的 uniform,用于呈现纹理。
Shader program:由 main 申明的一段程序源码,描述在片元上执行的操作。
在顶点着色器阶段只有唯一的 varying 输出变量-即内建变量:gl_FragColor。
1,精度上的差异
着色语言定了三种级别的精度:lowp, mediump, highp。我们可以在 glsl 脚本文件的开头定义默认的精度。如下代码定义在 float 类型默认使用 highp 级别的精度
precision highp float;
在顶点着色阶段,如果没有用户自定义的默认精度,那么 int 和 float 都默认为 highp 级别;而在片元着色阶段,如果没有用户自定义的默认精度,那么就真的没有默认精度了,我们必须在每个变量前放置精度描述符。此外,OpenGL ES 2.0 标准也没有强制要求所有实现在片元阶段都支持 highp 精度的。我们可以通过查看是否定义 GL_FRAGMENT_PRECISION_HIGH 来判断具体实现是否在片元着色器阶段支持 highp 精度,从而编写出可移植的代码。当然,通常我们不需要在片元着色器阶段使用 highp 级别的精度,推荐的做法是先使用 mediump 级别的精度,只有在效果不够好的情况下再考虑 highp 精度。
2,attribute 修饰符只可用于顶点着色。这个前面已经说过了。
3,或由于精度的不同,或因为编译优化的原因,在顶点着色和片元着色阶段同样的计算可能会得到不同的结果,这会导致一些问题(z-fighting)。因此 glsl 引入了 invariant 修饰符来修饰在两个着色阶段的同一变量,确保同样的计算会得到相同的值。
五,使用顶点着色器与片元着色器
好了,理论知识讲得足够多了,下面我们来看看如何在代码中添加顶点着色器与片元着色器。我们在前一篇文章《[OpenGL ES 01]OpenGL ES之初体验》代码的基础上进行编码。在前面提到可编程管线通过用 shader 语言编写脚本文件实现的,这些脚本文件相当于 C 源码,有源码就需要编译链接,因此需要对应的编译器与链接器,shader 对象与 program 对象就相当于编译器与链接器。shader 对象载入源码,然后编译成 object 形式(就像C源码编译成 .obj文件)。经过编译的 shader 就可以装配到 program 对象中,每个 program对象必须装配两个 shader 对象:一个顶点 shader,一个片元 shader,然后 program 对象被连接成“可执行文件”,这样就可以在 render 中是由该“可执行文件”了。
1,创建,装载和编译 shader首先,我们向工程中添加新的类 GLESUtils,让它继承自 NSObject。修改 GLESUtils.h 为:
#import <Foundation/Foundation.h> #include <OpenGLES/ES2/gl.h> @interface GLESUtils : NSObject // Create a shader object, load the shader source string, and compile the shader. // +(GLuint)loadShader:(GLenum)type withString:(NSString *)shaderString; +(GLuint)loadShader:(GLenum)type withFilepath:(NSString *)shaderFilepath; @end
修改 GLESUtils.m 为:
#import "GLESUtils.h" @implementation GLESUtils +(GLuint)loadShader:(GLenum)type withFilepath:(NSString *)shaderFilepath { NSError* error; NSString* shaderString = [NSString stringWithContentsOfFile:shaderFilepath encoding:NSUTF8StringEncoding error:&error]; if (!shaderString) { NSLog(@"Error: loading shader file: %@ %@", shaderFilepath, error.localizedDescription); return 0; } return [self loadShader:type withString:shaderString]; } +(GLuint)loadShader:(GLenum)type withString:(NSString *)shaderString { // Create the shader object GLuint shader = glCreateShader(type); if (shader == 0) { NSLog(@"Error: failed to create shader."); return 0; } // Load the shader source const char * shaderStringUTF8 = [shaderString UTF8String]; glShaderSource(shader, 1, &shaderStringUTF8, NULL); // Compile the shader glCompileShader(shader); // Check the compile status GLint compiled = 0; glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled); if (!compiled) { GLint infoLen = 0; glGetShaderiv ( shader, GL_INFO_LOG_LENGTH, &infoLen ); if (infoLen > 1) { char * infoLog = malloc(sizeof(char) * infoLen); glGetShaderInfoLog (shader, infoLen, NULL, infoLog); NSLog(@"Error compiling shader:\n%s\n", infoLog ); free(infoLog); } glDeleteShader(shader); return 0; } return shader; } @end
辅助类 GLESUtils 中有两个类方法用来跟进 shader 脚本字符串或 shader 脚本文件创建 shader,然后装载它,编译它。下面详细介绍每个步骤。
1),创建/删除 shader
函数 glCreateShader 用来创建 shader,参数 GLenum type 表示我们要处理的 shader 类型,它可以是 GL_VERTEX_SHADER 或 GL_FRAGMENT_SHADER,分别表示顶点 shader 或 片元 shader。它返回一个句柄指向创建好的 shader 对象。
函数 glDeleteShader 用来销毁 shader,参数为 glCreateShader 返回的 shader 对象句柄。
2),装载 shader
函数 glShaderSource 用来给指定 shader 提供 shader 源码。第一个参数是 shader 对象的句柄;第二个参数表示 shader 源码字符串的个数;第三个参数是 shader 源码字符串数组;第四个参数一个 int 数组,表示每个源码字符串应该取用的长度,如果该参数为 NULL,表示假定源码字符串是 \0 结尾的,读取该字符串的内容指定 \0 为止作为源码,如果该参数不是 NULL,则读取每个源码字符串中前 length(与每个字符串对应的 length)长度个字符作为源码。
3),编译 shader
函数 glCompileShader 用来编译指定的 shader 对象,这将编译存储在 shader 对象中的源码。我们可以通过函数 glGetShaderiv 来查询 shader 对象的信息,如本例中查询编译情况,此外还可以查询 GL_DELETE_STATUS,GL_INFO_LOG_STATUS,GL_SHADER_SOURCE_LENGTH 和 GL_SHADER_TYPE。在这里我们查询编译情况,如果返回 0,表示编译出错了,错误信息会写入 info 日志中,我们可以查询该 info 日志,从而获得错误信息。
2,编写着色脚本GLESUtils 提供的接口让我们可以使用两种方式:脚本字符串或脚本文件来提供 shader 源码,通常使用脚本文件方式有更大的灵活性。(Cocos2D 源码中倒是提供了不少脚本字符串应对一些常见的情况,有兴趣的同学可以查看下)。在这里,我们使用脚本文件方式。
1),添加顶点着色脚本
右击 Supporting Files 目录,New File->Other->Empty,输入名称:VertexShader.glsl,去除 target Tutorial02 中的勾选。后缀glsl 表示 GL Shader Language。
编辑其内容如下:
attribute vec4 vPosition; void main(void) { gl_Position = vPosition; }
然后选择 Tutorial02,在 Build Phases -> Copy Bundle Sources 中添加 VertexShader.glsl。
顶点着色脚本的源码很简单,如果你仔细阅读了前面的介绍,就一目了然。 attribute 属性 vPosition 表示从应用程序输入的类型为 vec4 的位置信息,输出内建 vary 变量 vPosition。留意:这里使用了默认的精度。
2),添加片元着色脚本
用于添加顶点着色脚本同样的方式添加名为 FragmentShader.glsl 的文件,编辑其内容如下:
precision mediump float; void main() { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); }
不用忘记在 Build Phases -> Copy Bundle Sources 中添加 FragmentShader.glsl。
片元着色脚本源码也很简单,前面说过片元着色要么自己定义默认精度,要么在每个变量前添加精度描述符,在这里自定义 float 的精度为 mediump。然后为内建输出变量 gl_FragColor 指定为红色。
3,创建 program,装配 shader,链接 program,使用 program1),创建 program
在 OpenGLView.h 的 OpenGLView 类声明中添加两个成员:
GLuint _programHandle; GLuint _positionSlot;
然后依然在 OpenGLView.m 中的匿名 category 中添加成员方法:
- (void)setupProgram;
在 - (void)render 方法前,添加其实现:
- (void)setupProgram { // Load shaders // NSString * vertexShaderPath = [[NSBundle mainBundle] pathForResource:@"VertexShader" ofType:@"glsl"]; NSString * fragmentShaderPath = [[NSBundle mainBundle] pathForResource:@"FragmentShader" ofType:@"glsl"]; GLuint vertexShader = [GLESUtils loadShader:GL_VERTEX_SHADER withFilepath:vertexShaderPath]; GLuint fragmentShader = [GLESUtils loadShader:GL_FRAGMENT_SHADER withFilepath:fragmentShaderPath]; // Create program, attach shaders. _programHandle = glCreateProgram(); if (!_programHandle) { NSLog(@"Failed to create program."); return; } glAttachShader(_programHandle, vertexShader); glAttachShader(_programHandle, fragmentShader); // Link program // glLinkProgram(_programHandle); // Check the link status GLint linked; glGetProgramiv(_programHandle, GL_LINK_STATUS, &linked ); if (!linked) { GLint infoLen = 0; glGetProgramiv (_programHandle, GL_INFO_LOG_LENGTH, &infoLen ); if (infoLen > 1) { char * infoLog = malloc(sizeof(char) * infoLen); glGetProgramInfoLog (_programHandle, infoLen, NULL, infoLog ); NSLog(@"Error linking program:\n%s\n", infoLog ); free (infoLog ); } glDeleteProgram(_programHandle); _programHandle = 0; return; } glUseProgram(_programHandle); // Get attribute slot from program // _positionSlot = glGetAttribLocation(_programHandle, "vPosition"); }
有了前面的介绍,上面的代码很容易理解。首先我们是由 GLESUtils 提供的辅助方法从前面创建的脚本中创建,装载和编译顶点 shader 和片元 shader;然后我们创建 program,将顶点 shader 和片元 shader 装配到 program 对象中,再使用 glLinkProgram 将装配的 shader 链接起来,这样两个 shader 就可以合作干活了。注意:链接过程会对 shader 进行可链接性检查,也就是前面说到同名变量必须同名同型以及变量个数不能超出范围等检查。我们如何检查 shader 编译情况一样,对 program 的链接情况进行检查。如果一切正确,那我们就可以调用 glUseProgram 激活 program 对象从而在 render 中使用它。通过调用 glGetAttribLocation 我们获取到 shader 中定义的变量 vPosition 在 program 的槽位,通过该槽位我们就可以对 vPosition 进行操作。
4,使用示例在 - (void)layoutSubviews 中调用 render 方法之前,插入对 setupProgram 的调用:
[self setupProgram]; [self render];
然后改写 render 方法:
- (void)render { glClearColor(0, 1.0, 0, 1.0); glClear(GL_COLOR_BUFFER_BIT); // Setup viewport // glViewport(0, 0, self.frame.size.width, self.frame.size.height); GLfloat vertices[] = { 0.0f, 0.5f, 0.0f, -0.5f, -0.5f, 0.0f, 0.5f, -0.5f, 0.0f }; // Load the vertex data // glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, vertices ); glEnableVertexAttribArray(_positionSlot); // Draw triangle // glDrawArrays(GL_TRIANGLES, 0, 3); [_context presentRenderbuffer:GL_RENDERBUFFER]; }
在新增的代码中,第一句 glViewport 表示渲染 surface 将在屏幕上的哪个区域呈现出来,然后我们创建一个三角形顶点数组,通过 glVertexAttribPointer 将三角形顶点数据装载到 OpenGL ES 中并与 vPositon 关联起来,最后通过 glDrawArrays 将三角形图元渲染出来。
5,编译运行编译运行,将看到一个红色的三角形显示在屏幕中央。知道为什么是红色的么?那是因为 program 也链接了片元着色器,在片元着色脚本文件中,我们指定 gl_FragColor 的值为红色 vec4(1.0, 0.0, 0.0, 1.0)。
在前文《[OpenGL ES 01]OpenGL ES之初体验》和本文中,我们详细了解了如何在 iPhone 中使用 OpenGL ES 的整个过程,包括设置 CAEAGLLayer 属性,创建 EAGLContext,创建和使用 renderbuffer 和 framebuffer,了解OpenGL ES 渲染管线,创建和使用 shader,创建和实现 program,使用顶点数组进行描绘。流程已经走通,接下来让我们进入 OpenGL ES 各个具体的技术领域。
本文源码可以在这里获得:https://github.com/kesalin/OpenGLES/tree/master/Tutorial02
OpenGL ES 2.0 Programming Guide
OpenGL ES Programming Guide for iOS
步步高vivo X1是最近很火爆的一台Android手机,它不像一般的Android手机般拼配置、拼价格,而是不走寻常路的拼音质。也算是第一台真正打入Hi-Fi大门的手机,它采用CD机级别的DAC芯片Cirrus Logic的CS4398以及CS8422作为SRC芯片,这也是很少在手机中见到的。当然,我们知道要研究音质,“唯芯论”是不可取的,音质还是需要真机试听才知道实力如何。一起跟着笔者进入X1的音乐世界吧。
vivo X1的正面造型和一般的Android手机较类似,但是要比所有的手机都纤薄,厚度仅有6.55mm,我们也拿来另外两台“刀锋”级别的手机MOTO Razr以及OPPO Finder来比一比,看看谁更“锋利”。
侧面对照,Razr由于摄像头处整块突起,所以显得不那么协调,而Finder摄像头突起,所以也显得很突兀,而X1则背部完全是平的,而且厚度最小。
当然,太薄也会出现一些小问题,例如这里的耳机插头就因为太大,把整台手机都架起来了。
回归到我们这次测试的主题——音质,这次测试我们选用的器材比较多,作为对比的手机有公认比较好的iPhone 5,耳机方面则有X1原配的MMX 71 iE、iPhone 5原配的earpods、Vsonic的VC02、Westone的UM1和UE10。
测试歌曲格式:iPhone 5使用AIFF、MP3,X1使用FLAC、MP3。
步步高vivo X1
X1作为少有的打“音质牌”的手机,在音乐播放功能当然会有过人之处。
X1的播放界面酷炫时尚,中间有动态的专辑封面显示。
点击专辑封面的位置以后出现进度条、音效选择、循环和随机播放等选项。
X1的音效选项很丰富,其中最大亮点当然是Hi-Fi模式,这个选项开启以后即会通过CS4398进行解码,而关闭则是通过软件算法进行解码,为了最大发挥X1的实力,以下测试以Hi-Fi模式为准。
BBE音效,打开以后人声齿音增多,低频不太自然,因此在测试中没有打开。
SRS音效,非常重口味,对低频的渲染过多。
iPhone 5
iPhone 5的播放界面和X1的挺类似,不过不同的是音效选项是在系统设置里面,内置音效也比较多,不过打开音效的感觉都不怎么好。
音质对比(以下对比X1选择Hi-Fi模式,不开任何音效,iPhone 5采用默认设置)
高频:X1的高频相对iPhone 5来说更加有质感,iPhone 5的高频显得比较干。X1的高频比iPhone 5更加华丽,但是高频的延伸上还有待提高。
中频:中频方面,X1比iPhone 5更厚,人声也更有感情。从调音上看,可以说X1更加味道一些,而iPhone 5相对来说稍微缺乏感情。
低频:X1的低频感觉稍软,虽然在低频量上能保证得不错,但是总觉得低频力度不怎么足。iPhone 5的低频量比较少,但是在低频上交代得比X1更加清晰,下潜更加深。而在动态表现方面,X1要比iPhone 5出色一些。
解析:X1在声音解析方面比iPhone 5要更加好,信息量更足。但是由于中频相对较厚和强劲的动态,给人一种解析不如iPhone 5的错觉。
声场:X1的横向声场要比iPhone 5更加宽,加上凌厉的动态,给人的感觉更加立体。但是在纵向声场上iPhone 5的表现要更加好些。
总体而言,X1的素质要比iPhone 5的更加高一些,当然这个结果也是笔者意料之中。X1整体声音走的是高保真路线,但加入了不少独特的调音风格,相信步步高的工程师在这方面也是下了不少功夫。但是和传统的Hi-Fi随身听还是有一定差距,当然对于一台手机,我们的要求不能这么苛刻。对于Android手机来说,X1的声音素质绝对是一个里程碑,要叫板iPhone 5也完全不是问题。X1的硬件素质非常优秀,接下来应该把调音做好,尤其在低音下潜方面应该加以提升,这样它的综合表现将会更加好。
X1适合人群:想要随身的高素质声音,但是又不想另外购买随身听的用户。
最近为了性能需求,开始搞JNI,白手起搞真心不容易。中间差点崩溃了好几次,最终总算得到一点心得。
JNI对性能的提升没有我预想中的大,对于for循环的速度提升大概在1倍左右,所以如果数量级不大的话,性能提升不会很明显
JNI编完之后,不能调试,是不是很蛋疼,不像android Java可以看出错信息。JNI crash之后,界面上表示为没有任何反应,过段时间直接退出应用,没有提示、也没有XXX已停止运行。第一次遇到真是无从下手,有没有!!!,经过对Java层的log研究发现,原来JNI crash后,cpu就直接死在那里(相当于assert,程序停止运行)。果断时间后JAVA层发现程序已挂之后(进程僵死),就强制杀死该进程。于是程序直接退出而无提示。(以上分析纯属个人分析,如有不对,敬请指正)
jni crash之后在logcat中会打出一段dump,这段dump没有tag,没有所属进程,所以如果ddms有filter会忽略这段dump而只看到
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 00000000
signal后面一般是11,目测应该是crash信号,code表示不同错误码,有linux基础应该能看懂
JNI crash dump 一般以***********************************开头
11-25 15:13:20.788: I/DEBUG(1825): *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
11-25 15:13:20.788: I/DEBUG(1825): Build fingerprint: 'samsung/GT-I9100/GT-I9100:4.0.3/IML74K/ZSLPG:user/release-keys'
11-25 15:13:20.788: I/DEBUG(1825): pid: 6454, tid: 6454 >>> com.ty.jnidebug <<<
11-25 15:13:20.788: I/DEBUG(1825): signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 00000000
11-25 15:13:20.788: I/DEBUG(1825): r0 0000f448 r1 95e00019 r2 00000001 r3 00000000
11-25 15:13:20.788: I/DEBUG(1825): r4 4c1c9a88 r5 000129d0 r6 00000000 r7 4be45da0
11-25 15:13:20.788: I/DEBUG(1825): r8 beafe668 r9 4be45d98 10 00000000 fp beafe67c
11-25 15:13:20.788: I/DEBUG(1825): ip 4c773bb9 sp beafe650 lr 40917cf4 pc 4c773bc6 cpsr 20000030
11-25 15:13:20.788: I/DEBUG(1825): d0 0000000080000000 d1 0000000080000000
11-25 15:13:20.788: I/DEBUG(1825): d2 0000000000000000 d3 0000000000000000
11-25 15:13:20.798: I/DEBUG(1825): d4 8000000000000000 d5 42dc00003f800000
11-25 15:13:20.798: I/DEBUG(1825): d6 00000000c2dc0000 d7 0000000000000000
11-25 15:13:20.798: I/DEBUG(1825): d8 0000000000000000 d9 0000000000000000
11-25 15:13:20.798: I/DEBUG(1825): d10 0000000000000000 d11 0000000000000000
11-25 15:13:20.798: I/DEBUG(1825): d12 0000000000000000 d13 0000000000000000
11-25 15:13:20.803: I/DEBUG(1825): d14 0000000000000000 d15 0000000000000000
11-25 15:13:20.803: I/DEBUG(1825): d16 000000000000000c d17 c05b800000000000
11-25 15:13:20.803: I/DEBUG(1825): d18 0000000000000000 d19 0000000000000000
11-25 15:13:20.803: I/DEBUG(1825): d20 3ff0000000000000 d21 8000000000000000
11-25 15:13:20.803: I/DEBUG(1825): d22 0000000000000000 d23 ff00ff00ff00ff00
11-25 15:13:20.803: I/DEBUG(1825): d24 ed12c639a9569e61 d25 ff00ff00ff00ff00
11-25 15:13:20.803: I/DEBUG(1825): d26 f5f5f5f5f5f5f5f5 d27 ffffffffffffffff
11-25 15:13:20.803: I/DEBUG(1825): d28 00f7003e00f1003d d29 3ff0000000000000
11-25 15:13:20.803: I/DEBUG(1825): d30 0000000000000000 d31 3ff0000000000000
11-25 15:13:20.803: I/DEBUG(1825): scr 20000012
11-25 15:13:21.003: I/DEBUG(1825): #00 pc 00000bc6 /data/data/com.ty.jnidebug/lib/libJNIDebug.so (Java_com_ty_jnidebug_MainActivity_test)
11-25 15:13:21.003: I/DEBUG(1825): #01 pc 0001ecf0 /system/lib/libdvm.so (dvmPlatformInvoke)
11-25 15:13:21.003: I/DEBUG(1825): #02 pc 00058fac /system/lib/libdvm.so (_Z16dvmCallJNIMethodPKjP6JValuePK6MethodP6Thread)
11-25 15:13:21.008: I/DEBUG(1825): code around pc:
11-25 15:13:21.008: I/DEBUG(1825): 4c773ba4 0000345c e59f0004 e08f0000 eaffffe5 \4..............
11-25 15:13:21.008: I/DEBUG(1825): 4c773bb4 0000344c 9001b086 23009100 9b049304 L4.........#....
11-25 15:13:21.008: I/DEBUG(1825): 4c773bc4 601a2201 93032300 9b03e004 9b039305 .".`.#..........
11-25 15:13:21.008: I/DEBUG(1825): 4c773bd4 93033301 4b029a03 ddf6429a 4770b006 .3.....K.B....pG
11-25 15:13:21.008: I/DEBUG(1825): 4c773be4 000003e7 e5903000 e3130101 13833102 .....0.......1..
11-25 15:13:21.008: I/DEBUG(1825): code around lr:
11-25 15:13:21.008: I/DEBUG(1825): 40917cd4 3497c004 3488c004 3afffff9 e2888004 ...4...4...:....
11-25 15:13:21.008: I/DEBUG(1825): 40917ce4 eafffff9 e899000c e59bc00c e12fff3c ............<./.
11-25 15:13:21.008: I/DEBUG(1825): 40917cf4 e3560000 159bc010 e24bd014 188c0003 ..V.......K.....
11-25 15:13:21.008: I/DEBUG(1825): 40917d04 e8bd8bc0 e1a0ce22 e59b6008 e2866001 ...."....`...`..
11-25 15:13:21.008: I/DEBUG(1825): 40917d14 e3a02000 e4d6c001 e35c0000 0a000007 . ........\.....
11-25 15:13:21.008: I/DEBUG(1825): stack:
11-25 15:13:21.008: I/DEBUG(1825): beafe610 beafe644 [stack]
11-25 15:13:21.008: I/DEBUG(1825): beafe614 40111091 /system/lib/libutils.so
11-25 15:13:21.008: I/DEBUG(1825): beafe618 001d2690 [heap]
11-25 15:13:21.008: I/DEBUG(1825): beafe61c 40094540
11-25 15:13:21.008: I/DEBUG(1825): beafe620 00000068
11-25 15:13:21.008: I/DEBUG(1825): beafe624 400944b4
11-25 15:13:21.008: I/DEBUG(1825): beafe628 001d2698 [heap]
11-25 15:13:21.008: I/DEBUG(1825): beafe62c 4be45cf4
11-25 15:13:21.008: I/DEBUG(1825): beafe630 00000000
11-25 15:13:21.008: I/DEBUG(1825): beafe634 40061ab9 /system/lib/libc.so
11-25 15:13:21.008: I/DEBUG(1825): beafe638 00000000
11-25 15:13:21.008: I/DEBUG(1825): beafe63c 00186258 [heap]
11-25 15:13:21.013: I/DEBUG(1825): beafe640 00000000
11-25 15:13:21.013: I/DEBUG(1825): beafe644 4be45cfc
11-25 15:13:21.013: I/DEBUG(1825): beafe648 df0027ad
11-25 15:13:21.013: I/DEBUG(1825): beafe64c 00000000
11-25 15:13:21.013: I/DEBUG(1825): #00 beafe650 95e00019
11-25 15:13:21.013: I/DEBUG(1825): beafe654 0000f448 [heap]
11-25 15:13:21.013: I/DEBUG(1825): beafe658 000129d0 [heap]
11-25 15:13:21.013: I/DEBUG(1825): beafe65c 00012a78 [heap]
11-25 15:13:21.013: I/DEBUG(1825): beafe660 00000000
11-25 15:13:21.013: I/DEBUG(1825): beafe664 4094d4d7 /system/lib/libdvm.so
11-25 15:13:21.013: I/DEBUG(1825): #01 beafe668 4be45d94
11-25 15:13:21.013: I/DEBUG(1825): beafe66c 00000001
11-25 15:13:21.013: I/DEBUG(1825): beafe670 414fc1a8 /dev/ashmem/dalvik-heap (deleted)
11-25 15:13:21.013: I/DEBUG(1825): beafe674 000129e0 [heap]
11-25 15:13:21.013: I/DEBUG(1825): beafe678 beafe8f8 [stack]
11-25 15:13:21.013: I/DEBUG(1825): beafe67c 40951faf /system/lib/libdvm.so
这段dump里面,包含了crash时cpu当时寄存器的信息,数据总线,callstack等信息,一般主要看lr pc寄存器来程序死在哪个代码上,看这么一堆地址自然看不出到底是哪个位置,这时我们需要一个symbol文件,sym文件包含了地址和函数入口的映射信息。为此google release了ndk-stack这个工具来查看死机时的callstack(其余的一些寄存器信息被过滤掉了,因为对于上层开发大部分没什么大用)
ndk-stack 使用方法
ndk-stack -sym <sym-file path> [-dump <dump-file-path>]
sym-file 一般用ndk-build编译出来生成在$project\obj\local\armeabi
dump-file就是logcat抓出来的log,注意里面需要有以***********************开头的信息,这个dump-file是可选的,如果没有指定则默认为默认输入流,那么可以这样用
adb logcat | $NDK/ndk-stack -sym $PROJECT_PATH/obj/local/armeabi
注意 “|”前后没有空格,否则会报错
更多信息可以查看ndk文档 your ndk-path/docs/NDK-STACK.html
JNI logging方法
在android.mk 里LOCAL_LDLIBS:= -llog
在C/C++源文件里面包含头文件#include <android/log.h>
为了使用方便,最好使用如下宏定义
#define LOG_TAG "libripplejni"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
这样,在logcat中的打印出来的JNI log 和 Java的就一样了
注意,如果实在framework下编JNI和使用NDK编译的方法有点不一样
注意实在android.mk中的定义和在include的头文件不一样。。。在framework中编写jni最好参考其他的JNI源码,并且注意要在AndroidRuntime.cpp中注册该函数