假设我们自定义一个OpenGL ES程序来处理图片,那么会有以下几个步骤:
1、初始化OpenGL ES环境,编译、链接顶点着色器和片元着色器;
2、缓存顶点、纹理坐标数据,传送图像数据到GPU;
3、绘制图元到特定的帧缓存;
4、在帧缓存取出绘制的图像。
GPUImageFilter
负责的是第一、二、三步。
GPUImageFramebuffer
负责是第四步。
一、GPUImageFilter解析
GPUImageFilter和响应链的其他元素实现了GPUImageInput
协议,他们都可以提供纹理参与响应链,或者从响应链的前面接收并处理纹理。响应链的下一个对象是target,响应链可能有多个分支(添加多个targets)。
Filters and other subsequent elements in the chain conform to the GPUImageInput protocol, which lets them take in the supplied or processed texture from the previous link in the chain and do something with it. Objects one step further down the chain are considered targets, and processing can be branched by adding multiple targets to a single output or filter.
- 获取纹理坐标
1 | + (const GLfloat *)textureCoordinatesForRotation:(GPUImageRotationMode)rotationMode; |
- 绘制结果输出
绘制的结果后输入到outputframebuffer
指定的缓存
usingNextFrameForImageCapture
代表着输出的结果会被用于获取图像,所以在绘制之前要加锁
1 | if (usingNextFrameForImageCapture) |
- 绑定纹理
glBindTexture(GL_TEXTURE_2D, [firstInputFramebuffer texture]);
绑定输入纹理,OpenGL ES才能确定要处理纹理数据 - 绑定顶点和纹理坐标并绘制图元
1 | glVertexAttribPointer(filterPositionAttribute, 2, GL_FLOAT, 0, 0, vertices); |
GL_TRIANGLE_STRIP
模式用于绘制三角形带。这里有介绍
- 纹理解锁
[firstInputFramebuffer unlock];
输入纹理使用完毕,解锁。在调用这个解锁之前必须确定之前已经调用加锁,否则会报错。GPUImageFramebuffer
使用引用计数来管理缓存,当引用计数小于0的时候会回收缓存。 - 信号量
如果设置了usingNextFrameForImageCapture
,则会通过GCD信号量来通知仍在等待绘制完成的函数。
1 | if (usingNextFrameForImageCapture) |
- 通知targets
- (void)informTargetsAboutNewFrameAtTime:(CMTime)frameTime;
当self的帧绘制完成后,通知自己的targets,并将自己的输出设置为targets的输入纹理:[self setInputFramebufferForTarget:currentTarget atIndex:textureIndex];
然后解锁自己使用的输出缓冲区[[self framebufferForOutput] unlock];
(在上一个函数已经lock了这个缓冲区,所以这里的unlock不会马上回收内存,等到targets使用完自己的纹理后调用unlock,缓存会被回收)
在设置完缓冲区后,self会通知所有targets(除了设置忽略的)[currentTarget newFrameReadyAtTime:frameTime atIndex:textureIndex];
- 等待渲染完成
1 | if (dispatch_semaphore_wait(imageCaptureSemaphore, convertedTimeout) != 0) |
- 一系列setter
- (void)setInteger:(GLint)newInteger forUniformName:(NSString *)uniformName;
这些函数是设置GLSL里面的变量
二、GPUImageFramebuffer
管理纹理缓存格式、帧缓存的buffer。
- 纹理格式
默认的纹理格式defaultTextureOptions
- 缓存创建
generateTexture
会创建对应的纹理缓存generateFramebuffer
会创建对应的帧缓存
注意:iOS5.0以上会使用CVOpenGLESTextureCache
否则会使用glTexImage2D()
,这个我们更熟悉的函数来传送CPU图像数据到GPU - 指定渲染目标
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _texture, 0);
把渲染目标指定为图像 - 调整视口大小
先绑定自己的帧缓存,再调整视口大小。
1 | - (void)activateFramebuffer; |
- 解锁
当引用计数小于1的时候,会调用下面的函数把自己放回缓存管理cache。(注意这个和 destroyFramebuffer不一样,一个是回收再利用,一个是销毁)
1 | [[GPUImageContext sharedFramebufferCache] returnFramebufferToCache:self]; |
- 从帧缓存中读取图片
在newCGImageFromFramebufferContents
函数获取图像数据。CVPixelBufferGetBaseAddress
和glReadPixels
都可以获得图像数据,根据iOS版本不同调用不同函数。
最后通过CGImageCreate,创建 CGImageRef,然后返回。 - CVPixelBuffer
CV像素缓存是一个主内存的图像缓存,应用在渲染帧、压缩解压视频、使用CoreImage都会用到CV像素缓存。
在访问CPU的像素数据之前,必须调用CVPixelBufferLockBaseAddress,并在访问后调用CVPixelBufferUnlockBaseAddress。如果lockFLags带有kCVPixelBufferLock_ReadOnly参数,那么unlocking 的时候也需要。
A Core Video pixel buffer is an image buffer that holds pixels in main memory. Applications generating frames, compressing or decompressing video, or using Core Image can all make use of Core Video pixel buffers.
- CVOpenGLESTextureCache
缓存和管理CVOpenGLESTextureRef纹理,这些纹理缓存提供了一个直接读写多种颜色格式缓存的方式。
Core Video OpenGLES texture caches are used to cache and manage CVOpenGLESTextureRef textures. These texture caches provide you with a way to directly read and write buffers with various pixel formats, such as 420v or BGRA, from GLES.
- CVOpenGLESTexture
CV纹理是纹理图像缓存,提供OpenGL图像数据
Core Video OpenGLES textures are texture-based image buffers used for supplying source image data to OpenGL.
扩展
GPUImage的四大输入基础类,都可以作为响应链的起点。这些基础类会把图像作为纹理,传给OpenGL ES处理,然后把纹理传递给响应链的下一个对象。GPUImageVideoCamera
摄像头-视频流GPUImageStillCamera
摄像头-照相GPUImagePicture
图片GPUImageMovie
视频
响应链,先要理解帧缓存的概念,这在OpenGL ES教程-帧缓存有提到过。
总结
用一句话来解释GPUImageFilter就是用来接收源图像,通过自定义的顶点、片元着色器来渲染新的图像,并在绘制完成后通知响应链的下一个对象。
GPUImageFramebuffer就是用来管理纹理缓存的格式与读写帧缓存的buffer。
上一篇介绍的是GPUImageFramebuffer
和GPUImageFilter
。
简单回顾一下:
GPUImageFilter
就是用来接收源图像,通过自定义的顶点、片元着色器来渲染新的图像,并在绘制完成后通知响应链的下一个对象。GPUImageFramebuffer
就是用来管理纹理缓存的格式与读写帧缓存的buffer。
这一篇介绍的是GPUImageVideoCamera
和GPUImageView
。
GPUImageVideoCamera
GPUImageVideoCamera是GPUImageOutput的子类,提供来自摄像头的图像数据作为源数据,一般是响应链的源头。
1、视频图像采集 :AVCaptureSession
GPUImage使用AVFoundation框架来获取视频。
AVCaptureSession类从AV输入设备的采集数据到制定的输出。
为了实现实时的图像捕获,要实现AVCaptureSession类,添加合适的输入(AVCaptureDeviceInput)和输出(比如 AVCaptureMovieFileOutput)
调用startRunning
开始输入到输出的数据流,调用stopRunning
停止数据流。
需要注意的是startRunning函数会花费一定的时间,所以不能在主线程(UI线程)调用,防止卡顿。
sessionPreset 属性可以自定义一些设置。
特殊的选项比如说高帧率,可以通过 AVCaptureDevice来设置。
AVCaptureSession使用的简单示例:
1 | _captureSession = [[AVCaptureSession alloc] init]; |
- AVCaptureVideoDataOutput
AVCaptureVideoDataOutput
是AVCaptureOutput
的子类,用来处理从摄像头采集的未压缩或者压缩过的图像帧。
通过captureOutput:didOutputSampleBuffer:fromConnection: delegate
,可以访问图像帧。
通过下面这个方法,可以设置delegate。
1 | - (void)setSampleBufferDelegate: |
需要注意的是,当一个新的视频图像帧被采集后,它会被传送到output,调用这里设置的delegate。所有的delegate函数会在这个queue中调用。如果队列被阻塞,新的图像帧到达后会被自动丢弃(默认alwaysDiscardsLateVideoFrames = YES)。这允许app处理当前的图像帧,不需要去管理不断增加的内存,因为处理速度跟不上采集的速度,等待处理的图像帧会占用内存,并且不断增大。
必须使用同步队列处理图像帧,保证帧的序列是顺序的。
- frameRenderingSemaphore 帧渲染的信号量
下面有一个这样的调用,用于等待处理完一帧后,再接着处理下一帧。
1 | if (dispatch_semaphore_wait(frameRenderingSemaphore, DISPATCH_TIME_NOW) != 0) |
- rotateCamera
前后摄像头翻转:更改videoInput的设置。
2、颜色空间:YUV
YUV是被欧洲电视系统所采用的一种颜色编码方法。
采用YUV色彩空间的重要性是它的亮度信号Y和色度信号U、V是分离的。如果只有Y信号分量而没有U、V分量,那么这样表示的图像就是黑白灰度图像。彩色电视采用YUV空间正是为了用亮度信号Y解决彩色电视机与黑白电视机的兼容问题,使黑白电视机也能接收彩色电视信号。
YCbCr或Y’CbCr有的时候会被写作:YCBCR或是Y’CBCR,是色彩空间的一种,通常会用于影片中的影像连续处理,或是数字摄影系统中。Y’为颜色的亮度(luma)成分、而CB和CR则为蓝色和红色的浓度偏移量成份。
YUV主要用于优化彩色视频信号的传输,使其向后相容老式黑白电视。与RGB视频信号传输相比,它最大的优点在于只需占用极少的频宽(RGB要求三个独立的视频信号同时传输)。
CbCr 则是在世界数字组织视频标准研制过程中作为ITU - R BT.601 建议的一部分,其实是YUV经过缩放和偏移的翻版。其中Y与YUV 中的Y含义一致,Cb,Cr 同样都指色彩,只是在表示方法上不同而已。在YUV 家族中,YCbCr 是在计算机系统中应用最多的成员,其应用领域很广泛,JPEG、MPEG均采用此格式。一般人们所讲的YUV大多是指YCbCr。YCbCr 有许多取样格式,如4∶4∶4,4∶2∶2,4∶1∶1 和4∶2∶0。
百度百科的介绍
YUV数据格式-图文详解
GPUImage中的YUVGLProgram *yuvConversionProgram;
将YUV颜色空间转换成RGB颜色空间的GLSL。CVPixelBufferGetPlaneCount()
返回缓冲区的平面数。
通过CVOpenGLESTextureCacheCreateTextureFromImage()
创建两个纹理luminanceTextureRef(亮度纹理)和chrominanceTextureRef(色度纹理)。convertYUVToRGBOutput()
把YUV颜色空间的纹理转换成RGB颜色空间的纹理
顶点着色器-通用kGPUImageVertexShaderString
片元着色器:
1、kGPUImageYUVFullRangeConversionForLAFragmentShaderString
2、kGPUImageYUVVideoRangeConversionForLAFragmentShaderString
区别在不同的格式
video-range (luma=[16,235] chroma=[16,240])
full-range (luma=[0,255] chroma=[1,255])
3、纹理绘制
glActiveTextue 并不是激活纹理单元,而是选择当前活跃的纹理单元。每一个纹理单元都有GL_TEXTURE_1D, 2D, 3D 和 CUBE_MAP。
1 | glActiveTexture(GL_TEXTURE1); |
GPUImageView
GPUImageView是响应链的终点,一般用于显示GPUImage的图像。
1、填充模式
GPUImageFillModeType fillMode
图像的填充模式。sizeInPixels
像素区域大小。recalculateViewGeometry()
重新计算图像顶点位置数据。AVMakeRectWithAspectRatioInsideRect()
在保证宽高比不变的前提下,得到一个尽可能大的矩形。
如果是kGPUImageFillModeStretch
图像拉伸,直接使宽高等于1.0即可,原图像会直接铺满整个屏幕。
如果是kGPUImageFillModePreserveAspectRatio
保持原宽高比,并且图像不超过屏幕。那么以当前屏幕大小为准。widthScaling = insetRect.size.width / currentViewSize.width;
如果是kGPUImageFillModePreserveAspectRatioAndFill
保持原宽高比,并且图像要铺满整个屏幕。那么图像大小为准。widthScaling = currentViewSize.height / insetRect.size.height;
imageVertices存放着顶点数据,上面的修改都会存放在这个数组。
2、OpenGL ES绘制
- (void)newFrameReadyAtTime:(CMTime)frameTime atIndex:(NSInteger)textureIndex;
源图像已经准备好,开始绘制。setDisplayFramebuffer()
会绑定GPUImageView的帧缓存,同时调试视口大小为view的大小。glActiveTexture
上面已经介绍过,是选择一个纹理单元。先选择纹理单元4,然后把源图像数据绑定到GL_TEXTURE_2D的位置上。最后告诉片元着色器,纹理单元是4。
1 | glActiveTexture(GL_TEXTURE4); |
这两行是分别绑定顶点坐标数据和纹理坐标数据。
1 | glVertexAttribPointer(displayPositionAttribute, 2, GL_FLOAT, 0, 0, imageVertices); |
这两行是设定输入的源图像数据缓存,并且对缓存加锁。
1 | inputFramebufferForDisplay = newInputFramebuffer; |
在准备好着色器、纹理data、顶点位置坐标和纹理坐标后,就可以调用glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
绘制图像。
GPUImageFilter
就是用来接收源图像,通过自定义的顶点、片元着色器来渲染新的图像,并在绘制完成后通知响应链的下一个对象。GPUImageFramebuffer
就是用来管理纹理缓存的格式与读写帧缓存的buffer。GPUImageVideoCamera
是GPUImageOutput
的子类,提供来自摄像头的图像数据作为源数据,一般是响应链的源头。GPUImageView
是响应链的终点,一般用于显示GPUImage的图像。
琨君的基于GPUImage的实时美颜滤镜对GPUImage实现美颜滤镜的原理和思路做了详细介绍。
本文以琨君的代码为demo,结合前两篇解析,探究美颜过程中的GPUImage实现。
GPUImage类介绍
1、GPUImageFilterGroup
GPUImageFilterGroup
是多个filter的集合,terminalFilter
为最终的filter,initialFilters
为filter数组。GPUImageFilterGroup
本身不绘制图像,对GPUImageFilterGroup
添加删除Target操作的操作都会转为terminalFilter
的操作。
2、GPUImageTwoInputFilter
GPUImageTwoInputFilter
是GPUImageFilter
的子类,对两个输入纹理进行通用的处理,需要继承它并准备自己的片元着色器。
两个输入纹理默认为inputImageTexture
和inputImageTexture2
。
- 重写了下面的函数,修改
GPUImageFilter
绘制的逻辑。
1 | - (void)renderToTextureWithVertices:(const GLfloat *)vertices |
下面这部分是核心的绘制逻辑:glActiveTexture()
是选择纹理单元,glBindTexture()
是把纹理单元和firstInputFramebuffer
、secondInputFramebuffer
管理的纹理内存绑定。glUniform1i()
告诉GLSL选择的纹理单元是2。
这部分在上一篇介绍也有提到,再详细阐述:glActiveTexture()
选择的是纹理单元,和glGenTextures()
返回的数字没有关系,可以在纹理单元2上面绑定纹理12。glGenTextures()
返回的纹理可以是GL_TEXTURE_2D
类型也可以是GL_TEXTURE_CUBE_MAP
类型,取决于glBindTexture()
第一次绑定纹理的是GL_TEXTURE_2D
还是GL_TEXTURE_CUBE_MAP
。
1 | glActiveTexture(GL_TEXTURE2); |
nextAvailableTextureIndex
用于获取下一个纹理索引
1 | - (NSInteger)nextAvailableTextureIndex; |
setInputFramebuffer: atIndex:
会根据上面获取的textureIndex
设置firstInputFramebuffer
和secondInputFramebuffer
。如果是textureIndex = 0
,设置hasSetFirstTexture
表示已经设置第一个纹理。
3、GPUImageThreeInputFilter
GPUImageThreeInputFilter
的逻辑与GPUImageTwoInputFilter
类似,增加了thirdInputFramebuffer
作为第三个纹理inputImageTexture3
的输入。
4、GPUImageBeautifyFilter
GPUImageBeautifyFilter
是基于GPUImage的实时美颜滤镜中的美颜滤镜,包括GPUImageBilateralFilter
、GPUImageCannyEdgeDetectionFilter
、GPUImageCombinationFilter
、GPUImageHSBFilter
。
绘制流程
绘制流程图
- 1、
GPUImageVideoCamera
捕获摄像头图像
调用newFrameReadyAtTime: atIndex:
通知GPUImageBeautifyFilter
; - 2、
GPUImageBeautifyFilter
调用newFrameReadyAtTime: atIndex:
通知GPUImageBilateralFliter
输入纹理已经准备好; - 3、
GPUImageBilateralFliter
绘制图像后在informTargetsAboutNewFrameAtTime()
,
调用setInputFramebufferForTarget: atIndex:
把绘制的图像设置为GPUImageCombinationFilter
输入纹理,
并通知GPUImageCombinationFilter
纹理已经绘制完毕; - 4、
GPUImageBeautifyFilter
调用newFrameReadyAtTime: atIndex:
通知GPUImageCannyEdgeDetectionFilter
输入纹理已经准备好; - 5、同3,
GPUImageCannyEdgeDetectionFilter
绘制图像后,
把图像设置为GPUImageCombinationFilter
输入纹理; - 6、
GPUImageBeautifyFilter
调用newFrameReadyAtTime: atIndex:
通知GPUImageCombinationFilter
输入纹理已经准备好; - 7、
GPUImageCombinationFilter
判断是否有三个纹理,三个纹理都已经准备好后
调用GPUImageThreeInputFilter
的绘制函数renderToTextureWithVertices: textureCoordinates:
,
图像绘制完后,把图像设置为GPUImageHSBFilter
的输入纹理,
通知GPUImageHSBFilter
纹理已经绘制完毕; - 8、
GPUImageHSBFilter
调用renderToTextureWithVertices: textureCoordinates:
绘制图像,
完成后把图像设置为GPUImageView
的输入纹理,并通知GPUImageView
输入纹理已经绘制完毕; - 9、
GPUImageView
把输入纹理绘制到自己的帧缓存,然后通过[self.context presentRenderbuffer:GL_RENDERBUFFER];
显示到UIView
上。
总结
GPUImageFilter
GPUImageFramebuffer
GPUImageVideoCamera
GPUImageView
GPUImageFilterGroup
GPUImageTwoInputFilter
GPUImageThreeInputFilter
这是学习这个demo需要了解的7个类。
在绘制流程图的过程中,对GPUImage的响应链有了更清晰的认识。
这次介绍的GPUImageContext
、GPUImageFramebufferCache
和GPUImagePicture
。
GPUImageContext
GPUImageContext是GPUImage对OpenGL ES上下文的封装,添加了GPUImage相关的上下文,比如说Program的使用缓存,处理队列,CV纹理缓存等。
1、属性介绍
contextQueue
统一处理队列currentShaderProgram
正在使用的programcontext
OpenGL ES的上下文coreVideoTextureCache
CV纹理缓存framebufferCache
GPUImageBuffer缓存shaderProgramCache
Program的缓存shaderProgramUsageHistory
Program的使用历史
2、方法介绍
useAsCurrentContext()
在useAsCurrentContext设置当前上下文的时候,会先判断上下文是否是当前context,不是再设置(为了避免上下文切换的性能消耗,即使设置的上下文是同一个上下文也会消耗性能)sizeThatFitsWithinATextureForSize()
会调整纹理大小,如果超过最大的纹理,会调整为不超过最大的纹理宽高。(GLProgram*)programForVertexShaderString:fragmentShaderString:;
shaderProgramCache 是program的缓存,由顶点shader和片元shader字符串拼接起来做key。- (void)useSharegroup:(EAGLSharegroup *)sharegroup;
EAGLSharegroup类管理一个或者多个EAGLContext的OpenGLES资源;这个是一个封闭的类,没有开发者API。负责管理纹理缓存、顶点缓存、帧缓存、颜色缓存。(textures, buffers, framebuffers, and render buffers)- (EAGLContext *)context;
返回OpenGL ES2.0的上下文,同时设置glDisable(GL_DEPTH_TEST);
,图像处理管道默认不允许使用深度缓存。
GPUImageFramebufferCache
GPUImageFramebufferCache是GPUImageFrameBuffer的管理类
1、属性介绍
CacheframebufferCache
缓存字典framebufferTypeCounts
缓存数量字典activeImageCaptureList
正在读取Image数据的GPUImageFrameBuffer列表framebufferCacheQueue
缓存队列
2、方法介绍
- (NSString *)hashForSize: textureOptions:onlyTexture:;
根据size、textureOptions和onlyTexture,创建缓存字符串。
缓存字符串+当前缓存数量形成framebufferCache缓存的key。
如果找不到framebufferCache对应的数量,会创建新的缓存。- (void)returnFramebufferToCache:;
回收缓存。根据size、textureOptions和onlyTexture,创建缓存字符串,缓存字符串+当前缓存数量形成framebufferCache缓存的key。(之所以会加上数量,是因为缓存字符串不唯一)- (void)addFramebufferToActiveImageCaptureList:;
- (void)removeFramebufferFromActiveImageCaptureList:
这两个方法主要用于,当newCGImageFromFramebufferContents()
读取帧缓存图像数据时,保持GPUImageFramebuffer的引用。并且读取完数据后,在dataProviderUnlockCallback()
方法释放。
GPUImagePicture
GPUImagePicture是PGUImage的图像处理类,继承GPUImageOutput,一般作为响应链的源头。
1、属性介绍
pixelSizeOfImage 图像的像素大小。
hasProcessedImage 图像是否已处理。
imageUpdateSemaphore 图像处理的GCD信号量。
2、方法介绍
- (id)initWithCGImage:smoothlyScaleOutput:
用源图像newImageSource和是否采用mipmaps来初始化GPUImagePicture。
如果图像大小超过OpenGL ES最大纹理宽高,或者使用mipmaps,或者图像数据是浮点型、颜色空间不对等都会采用CoreGraphics重新绘制图像。
然后通过glTexImage2D把图像数据发送给GPU,最后释放掉CPU的图像数据。- (BOOL)processImageWithCompletionHandler:;
通知targets处理图像,并在完成后调用complete代码块。在处理开始时,会标记hasProcessedImage为YES,并调用dispatch_semaphore_wait()
,确定上次处理已经完成,否则取消这次处理。
- (void)addTarget: atTextureLocation:;
添加target到响应链。如果hasProcessedImage为YES,表示图像已经处理完毕,直接设置targets的InputSize,并调用newFrameReadyAtTime()
通知target。
将GPUImageView设置为self.view,根据face.png,设置GPUImagePicture,然后添加GPUImageTiltShiftFilter到响应链,再把GPUImageView作为响应链的终点,最后调用processImage,开始处理图像。
1 | GPUImageView *primaryView = [[GPUImageView alloc] initWithFrame:self.view.frame]; |
总结
最近因为直播用户增长太快,忙着优化原来的逻辑,研读源代码的时间变少。
同时为了写这篇文章,查了一些关于图像资料,末尾附上。
下一篇文章可能会介绍今年大火的直播APP的一种速成方案,也可能会是GPUImageMovie的介绍。
喜欢的点一下关注,不迷路。
Mipmap纹理技术是目前解决纹理分辨率与视点距离关系的最有效途径,它会先将图片压缩成很多逐渐缩小的图片,例如一张6464的图片,会产生6464,3232,1616,88,44,22,11的7张图片,当屏幕上需要绘制像素点为2020 时,程序只是利用 3232 和 1616 这两张图片来计算出即将显示为 2020 大小的一个图片,这比单独利用 32*32 的那张原始片计算出来的图片效果要好得多,速度也更快.
kCGImageAlphaLast:alpha 分量存储在每个像素中的低位,如RGBA。
kCGImageAlphaFirst:alpha 分量存储在每个像素中的高位,如ARGB。
kCGImageAlphaPremultipliedLast:alpha 分量存储在每个像素中的低位,同时颜色分量已经乘以了 alpha 值。
kCGImageAlphaPremultipliedFirst:alpha 分量存储在每个像素中的高位,同时颜色分量已经乘以了 alpha 值。
kCGImageAlphaNoneSkipLast:没有 alpha 分量。如果像素的总大小大于颜色空间中颜色分量数目所需要的空间,则低位将被忽略。
kCGImageAlphaNoneSkipFirst:没有 alpha 分量。如果像素的总大小大于颜色空间中颜色分量数目所需要的空间,则高位将被忽略。