Video Toolbox:读写解码回调函数CVImageBufferRef的YUV图像
本文档基于H.264的解码,介绍读写Video Toolbox解码回调函数参数CVImageBufferRef中的YUV或RGB数据的方法,并给出CVImageBufferRef生成灰度图代码、方便调试。同时,还介绍了Video Toolbox解码回调中进行YUV处理时容易忽略的问题。文档定位于iOS音视频高级编程,致力于提供高参考价值的Core Video中文资料,最近也在StackOverflow上关注Core Video相关问题,学习并回馈社区。
目录
|- 读取CVImageBufferRef(CVPixelBufferRef)
|- 写入CVImageBufferRef(CVPixelBufferRef)
|- CVPixelBufferPool内存池
|- CVPixelBuffer通过Core Graphics创建灰度图
|- 坑
|-- 直接操作解码回调的CVImageBuffer(CVPixelBuffer)存在的问题
|-- CVPixelBuffer上传至GPU后图像垂直镜像问题
|- 参考与推荐阅读
在实现全景视频播放器及其关联项目过程中,我编写了以下Video Toolbox相关文档(因开发任务等原因,部分文档处于草稿状态,之后会进行内容修订):
CVPixelBufferRef是CVImageBufferRef的别名,两者操作几乎一致。
// CVPixelBuffer.h/*
* CVPixelBufferRef
* Based on the image buffer type.
* The pixel buffer implements the memory storage for an image buffer.
*/typedef CVImageBufferRef CVPixelBufferRef;
虽然语法上CVPixelBufferRef是CVImageBufferRef的别名,它们在文档中的说明却有区别:
Core Video image buffers provides a convenient interface for managing different types of image data. Pixel buffers and Core Video OpenGL buffers derive from the Core Video image buffer.
CVImageBufferRef:A reference to a Core Video image buffer. An image buffer is an abstract type representing Core Video buffers that hold images. In Core Video, pixel buffers, OpenGL buffers, and OpenGL textures all derive from the image buffer type.
CVPixelBufferRef :A reference to a Core Video pixel buffer object. The pixel buffer stores an image in main memory.
从上述可知,CVPixelBuffer『继承了』CVImageBuffer,然而,由于Core Video暴露出来的是Objective-C接口,意味着若想用C语言实现『面向对象的继承』,则CVPixelBuffer的数据成员定义位置与CVImageBuffer基本保持一致且令编译器进行相同的偏移以确保字节对齐,犹如FFmpeg中AVFrame可强制转换成AVPicture,以FFmpeg 3.0源码为例。
typedef struct AVFrame {
uint8_t *data[AV_NUM_DATA_POINTERS];
int linesize[AV_NUM_DATA_POINTERS];
uint8_t **extended_data; // 后续还有众多字段
}
typedef struct AVPicture {
///< pointers to the image data planes
uint8_t *data[AV_NUM_DATA_POINTERS];
///< number of bytes per line
int linesize[AV_NUM_DATA_POINTERS];
} AVPicture;
当然,从苹果开源的某些框架上看,Core Video内部极有可能用Objective-C++实现,可能真正用了C++式继承,在此不作过多猜测。
1、读取CVImageBufferRef(CVPixelBufferRef)
在解码回调中,传递过来的帧数据由CVImageBufferRef指向。如果需取出其中像素数据作进一步处理,得访问其中真正存储像素的内存。
VideoToolbox解码后的图像数据并不能直接给CPU访问,需先用CVPixelBufferLockBaseAddress()锁定地址才能从主存访问,否则调用CVPixelBufferGetBaseAddressOfPlane等函数则返回NULL或无效值。值得注意的是,CVPixelBufferLockBaseAddress自身的调用并不消耗多少性能,一般情况,锁定之后,往CVPixelBuffer拷贝内存才是相对耗时的操作,比如计算内存偏移。如果CVPixelBuffer的图像需要显示在屏幕上,建议用GPU实现图像处理操作。下面展示读写左半图像时的性能损耗(请忽略内存计算的粗暴代码)。
读取CVPixelBuffer图像的性能消耗
aaarticlea/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyBpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkJDQzA1MTVGNkE2MjExRTRBRjEzODVCM0Q0NEVFMjFBIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkJDQzA1MTYwNkE2MjExRTRBRjEzODVCM0Q0NEVFMjFBIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6QkNDMDUxNUQ2QTYyMTFFNEFGMTM4NUIzRDQ0RUUyMUEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6QkNDMDUxNUU2QTYyMTFFNEFGMTM4NUIzRDQ0RUUyMUEiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz6p+a6fAAAAD0lEQVR42mJ89/Y1QIABAAWXAsgVS/hWAAAAAElFTkSuQmCC" alt="" data-ratio="0.09919354838709678" data-src="http://mmbiz.qpic.cn/mmbiz_jpg/g4uoJOMA38LPmJbibFZic9kLAgoMs3lqBnEmLNWB0HTksyOI343BqDcRiaAnm0Yp5CZp2M79UTCy7dt2xdp3a06kg/0?wx_fmt=jpeg" data-type="jpeg" data-w="1240" />
写入CVPixelBuffer图像的性能消耗
然而,用CVImageBuffer -> CIImage -> UIImage则无需显式调用锁定基地址函数。
// CVPixelBufferLockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly); // 可以不加
CIImage *ciImage = [CIImage imageWithCVPixelBuffer:imageBuffer];
CIContext *temporaryContext = [CIContext contextWithOptions:nil];CGImageRef videoImage = [temporaryContext
createCGImage:ciImage
fromRect:CGRectMake(0, 0,
CVPixelBufferGetWidth(imageBuffer),
CVPixelBufferGetHeight(imageBuffer))];
UIImage *image = [[UIImage alloc] initWithCGImage:videoImage];
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
CGImageRelease(videoImage);
// CVPixelBufferUnlockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
CVPixelBufferIsPlanar可得到像素的存储方式是Planar或Chunky。若是Planar,则通过CVPixelBufferGetPlaneCount获取YUV Plane数量。通常是两个Plane,Y为一个Plane,UV由VTDecompressionSessionCreate创建解码会话时通过destinationImageBufferAttributes指定需要的像素格式(可不同于视频源像素格式)决定是否同属一个Plane,每个Plane可当作表格按行列处理,像素是行顺序填充的。下面以Planar Buffer存储方式作说明。
CVPixelBufferGetPlaneCount得到像素缓冲区平面数量,然后由CVPixelBufferGetBaseAddressOfPlane(索引)得到相应的通道,一般是Y、U、V通道存储地址,UV是否分开由解码会话指定,如前面所述。而CVPixelBufferGetBaseAddress返回的对于Planar Buffer则是指向PlanarComponentInfo结构体的指针,相关定义如下:
/*
Planar pixel buffers have the following descriptor at their base address.
Clients should generally use CVPixelBufferGetBaseAddressOfPlane,
CVPixelBufferGetBytesPerRowOfPlane, etc. instead of accessing it directly.
*/
struct CVPlanarComponentInfo {
/* offset from main base address to base address of this plane, big-endian */
int32_t offset;
/* bytes per row of this plane, big-endian */
uint32_t rowBytes;
};
typedef struct CVPlanarComponentInfo CVPlanarComponentInfo;struct CVPlanarPixelBufferInfo {
CVPlanarComponentInfo componentInfo[1];
};
typedef struct CVPlanarPixelBufferInfo CVPlanarPixelBufferInfo;struct CVPlanarPixelBufferInfo_YCbCrPlanar {
CVPlanarComponentInfo componentInfoY;
CVPlanarComponentInfo componentInfoCb;
CVPlanarComponentInfo componentInfoCr;
};
typedef struct CVPlanarPixelBufferInfo_YCbCrPlanar CVPlanarPixelBufferInfo_YCbCrPlanar;struct CVPlanarPixelBufferInfo_YCbCrBiPlanar {
CVPlanarComponentInfo componentInfoY;
CVPlanarComponentInfo componentInfoCbCr;
};
typedef struct CVPlanarPixelBufferInfo_YCbCrBiPlanar CVPlanarPixelBufferInfo_YCbCrBiPlanar;
根据CVPixelBufferGetPixelFormatType得到像素格式,以对应的方式读取,比如YUV420SP跨距读取所有的U到一个缓冲区。
2、写入CVImageBufferRef(CVPixelBufferRef)
下面代码展示了以向Y、UV Planar拷贝数据的过程:
NSDictionary *pixelAttributes = @{(id)kCVPixelBufferIOSurfacePropertiesKey : @{}};
CVPixelBufferRef pixelBuffer = NULL;
CVReturn result = CVPixelBufferCreate(kCFAllocatorDefault,
width, height,
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
(__bridge CFDictionaryRef)pixelAttributes)
&pixelBuffer);
CVPixelBufferLockBaseAddress(pixelBuffer, 0);uint8_t *yDestPlane = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0);memcpy(yDestPlane, yPlane, width * height);uint8_t *uvDestPlane = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1);// numberOfElementsForChroma为UV宽高乘积memcpy(uvDestPlane, uvPlane, numberOfElementsForChroma);
CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
if (result != kCVReturnSuccess) {
NSLog(@"Unable to create cvpixelbuffer %d", result);
}
CIImage *coreImage = [CIImage imageWithCVPixelBuffer:pixelBuffer];
CVPixelBufferRelease(pixelBuffer);
上述代码通过- [CIImage imageWithCVPixelBuffer:]
创建CIImage在iPad Air 2、iPhone 6p等真机上存在的问题:
1、当使用kCVPixelFormatType_420YpCbCr8PlanarFullRange时提示[CIImage initWithCVPixelBuffer:options:] failed because its pixel format f420 is not supported.
,即不支持由YUV420P格式的CVPixelBuffer创建CIImage。
经测试,视频源格式为yuvj420p(pc, bt709),在VTDecompressionSessionCreate不指定destinationImageBufferAttributes的kCVPixelBufferPixelFormatTypeKey值时,Video Toolbox解码出来的CVImageBufferRef对应为f420。
当指定destinationImageBufferAttributes需要kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange时,解码出来的ImageBuffer为420v,然后创建YUV时指定PixelFormat为f420会出现上述问题。原因是,以420v方式拷贝YUV数据,其存储布局与f420不同,导致创建CIImage失败。
2、决定CVPixelBufferCreate创建的格式是其参数pixelFormatType,而非参数pixelAttributes使用kCVPixelBufferPixelFormatTypeKey指定的像素格式。
下面介绍一些简单的图像处理办法。
原始灰度图
(一)水平镜像
水平镜像就是图像绕图像中间垂直线交换左右像素点位置,使用矩阵运行表示为:
[x, y, 1] -1 0 0 -> [x', y', 1]
0 1 0
width 0 1
对于CPU而言,矩阵运行通常没GPU快,因为GPU做2x2、3x3等矩阵运算是硬件加速实现的,很可能就是一条指令处理完,而CPU往往是逐个元素进行计算,因此,目前大家倾向于GPU做矩阵运行。示例CPU实现代码如下。
for (int line = 0; line < 480; ++line) {
for (int col = 0; col < 960; ++col) {
dst_buffer[line * 960 + col] = src_buffer[line * 960 + (960 - col)];
}
}
水平镜像
(二)垂直镜像
垂直镜像就是图像绕图像中间水平线交换上下像素点位置,使用矩阵运行表示为:
[x, y, 1] 1 0 0 -> [x', y', 1]
0 -1 0
0 height 1
示例CPU实现代码如下。
for (int line = 0; line < 480; ++line) {
for (int col = 0; col < 960; ++col) {
dst_buffer[(480 - line) * 960 + col] = src_buffer[line * 960 + col];
}
}
垂直镜像
3、CVPixelBufferPool内存池
自行创建CVPixelBufferPool且通过CVPixelBufferPool创建CVPixelBuffer,容易出现CVPixelBuffer被错误释放或意外增加引用计数导致内存泄露,以ijkplayer为例演示CVPixelBubffer泄露的情况。
CVPixelBuffer泄露
CVPixelBuffer结束引用时引用计数不为0导致内存泄露
而自行创建CVPixelBuffer,则容易出现内存暴涨问题,如创建一个960x480的YUV420SP格式的CVPixelBuffer所占内存为700多M,如果是异步解码且没作内存大小限制,将导致应用崩溃。
CVPixelBufferCreate占用的内存
如果不想自行创建CVPixelBufferPool,也不想自己创建CVPixelBuffer,取巧的办法是,使用解码回调函数的CVPixelBuffer,则无需担心内存消耗问题。在实践过程中,图像处理后立即编码,这样使用的场合不会导致解码器自身的缓存队列数据出现图像紊乱。前提是,修改后的像素数据在原数据的宽高范围内。当然,这也会出现些问题,具体在文档后续部分进行讨论
对于解码->图像处理->编码流程,且处理后的图像与原图像大小不同,则创建编码器时再创建CVPixelBufferPool,让系统管理CVPixelBuffer也是可靠的做法。
另外,在图像处理过程中,Video Toolbox无论指定FullRange还是VideoRange,由此通过Core Graphics创建RGB图像是正确的,和QuickTime播放时画面保持一致。然而,解码出来的YUV420SP数据经过拷贝,接着进行图像处理,存在部分区域颜色有误。通过指定Video Toolbox输出YUV420P,再进行图像处理则无颜色异常问题。当然,使用的算法也改变相应的YUV420P算法,因为个人认为,这极有可能是我们团队的YUV420SP拷贝及操作算法有误。
4、CVPixelBuffer通过Core Graphics创建灰度图
修改完YUV数据后,如果每次都需要GPU实现YUV转换RGB,这比较麻烦,特别是转码等离线计算场合。下面,介绍一种实现CVPixelBuffer生成UIImage的办法,只使用Y平面生成图像,判断图像成像方面的处理结果是否符合预期。
// baseAddress为Y平面地址,传递yuv420(s)p完整数据地址,则忽略uv
UIImage* yuv420ToUIImage(void *baseAddress, size_t width, size_t height, size_t bytesPerRow) { // Create a device-dependent gray color space
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray(); // Create a bitmap graphics context with the sample buffer data
CGContextRef context = CGBitmapContextCreate(baseAddress,
width, height,
8,
bytesPerRow,
colorSpace,
kCGImageAlphaNone); // Create a Quartz image from the pixel data in the bitmap graphics context
CGImageRef quartzImage = CGBitmapContextCreateImage(context); // Free up the context and color space
CGContextRelease(context);
CGColorSpaceRelease(colorSpace); // Create an image object from the Quartz image
UIImage *image = [UIImage imageWithCGImage:quartzImage]; return image;
}
上述代码可能会引起这样的疑问:灰度图为何不需要U和V通道的数据。确实,此问题我最近特意查阅了些资料。创建灰度图时,有些人还将U、V通道在偏置前(值范围[-128, 127])设置为0,或者偏置后(值范围[0, 255])设置为128,然而,创建灰度图时,他们的代码并未使用UV数据。另外,看到一种说法是:
Y通道就是平时所说的灰度通道。
当然,以我有限的了解来看,个人不太认可这种说法。原因是,Y通道是YUV的一个分量,而灰度是复合量,即使数值接近,在概念上应该也是有区别的。数值接近的意思是,以BT. 601转换矩阵为例进行证明:
Y = 0.299R + 0.587G + 0.114B
GrayScale = (R + G + B) / 3
可见,Y值在数值接近灰度值。下面,对创建图像的代码段进行简要分析。
一些开源项目,如SDWebImage,它使用CGColorSpaceCreateDeviceRGB函数,是因为它的数据源是RGB,而我们这里的YUV数据需要经过颜色转换矩阵运算才能得到RGB,简单起见,由CGColorSpaceCreateDeviceGray函数创建灰度图可直接看到图像发生的变化,缺点是,丢失了颜色信息。示意图如下所示。
生成灰度图
虽然,像素格式为YUV的视频解码后几乎都可生成灰度图。然而,并不是所有的图像原始数据都能通过Core Graphics生成可视图像,iOS支持的像素格式非常有限,如下所示。
生成灰度图支持的像素格式
5、坑
操作CVImageBuffer(CVPixelBuffer)虽然看着没什么难度,然而,还是有些大大小小的问题。如果对此不作描述,那么本文档的标题真是太标题党了。下面,给出我在开发过程中遇到并解决的情况。
5.1、直接操作解码回调的CVImageBuffer(CVPixelBuffer)存在的问题
在解码回调函数中进行YUV处理,无论是否同步解码,或者解码与创建纹理、刷新界面是否为同一线程。需要注意的是,解码回调得到的CVPixelBuffer中的图像是上一次解码回调中处理过的图像,而非视频压缩数据通过解码得到的新的完整图像。换句话说,在一个关键帧解码成功后,其后续P帧以前一帧为基础,继续解码并将结果叠加到新画面,然后传递到解码回调函数。简单示意之。
Decode Thread: VTDecompressionSessionDecodeFrame -> VTDecoderCallback (进行图像处理) -> 添加到待显示队列
Rendering Thread: 读取待显示队列、得到已处理的CVPixelBuffer -> CVOpenGLESTextureCacheCreateTextureFromImage
下面,详细讨论上述情况。进行YUV三个通道处理后,播放出来的画面看着正常,相关资源占用信息如下所示。然而,经输出Video Toolbox回调函数传递过来的CVPixelBuffer或说CVImageBuffer,发现是之前我们处理过的图像,并在上一关键帧基础上持续叠加P帧,把结果图像作为下一帧视频。
CPU不超负荷的资源占用
CPU不超负荷的GPU占用
CPU不超负荷的Y通道图
CPU不超负荷的解码回调每帧图像
可见,作为一个关键帧间隔为15的视频序列,src_1.jpg与src_16.jpg因关键帧得到一次立即刷新,随后的图像都在YUV处理的基础上持续叠加。
5.2、CVPixelBuffer上传至GPU后图像垂直镜像问题
对于CMVideoFormatDescription及指定输出的CVPixelBuffer信息如下的解码过程,在自行创建CVPixelBuffer后,将解码回调函数的CVPixelBuffer数据拷贝到新CVPixelBuffer,通常会遇到图像颠倒了,确切地说,图像出现垂直镜像问题。不过,使用前面生成灰度图函数得到的图像都是正的,不存在颠倒,只有上传到GPU里才存在此现象。原因是,计算机的图像存储时有自己的坐标,这个坐标与OpenGL ES的纹理坐标的Y轴正好相反,故图像在GPU中是颠倒的。
CMVideoFormatDescription {
CVFieldCount = 1;
CVImageBufferChromaLocationBottomField = Left;
CVImageBufferChromaLocationTopField = Left;
FullRangeVideo = 0;
SampleDescriptionExtensionAtoms = {
avcC = <01640033 ffe10014 67640033 ac1b4583 c0f68400 000fa000 03a98010 01000468 e923cbfd f8f800>;
};
}
destinationImageBufferAttributes = {
OpenGLESCompatibility = 1;
PixelFormatType = 2033463856;
}
现在,尝试使用Core Video接口处理此问题。首先,判断源及目标图像是否翻转。
bool isFlipped = CVImageBufferIsFlipped(pixelBuffer);
if (isFlipped) {
NSLog(@"pixelBuffer is %s", isFlipped ? "flipped" : "not flipped"); }
isFlipped = CVImageBufferIsFlipped(imageBuffer);
if (isFlipped) {
NSLog(@"imageBuffer is %s", isFlipped ? "flipped" : "not flipped");
}
发现图像都是翻转的,执行结果所下。
pixelBuffer is flipped
imageBuffer is flipped
显然,还需要更多信息去判断。再获取两个缓冲区的ShouldNotPropagate属性,发现都没有值。但是,回调函数的像素缓冲区有ShouldPropagate属性,而我们自行创建的缓冲区则无此属性,如下所示。
CVFieldCount = 1;
CVImageBufferChromaLocationBottomField = Left;
CVImageBufferChromaLocationTopField = Left;
CVImageBufferColorPrimaries = "SMPTE_C";
CVImageBufferTransferFunction = "ITU_R_709_2";
CVImageBufferYCbCrMatrix = "ITU_R_601_4";
ColorInfoGuessedBy = VideoToolbox;
那么,根据H.264文档,CVFieldCount只是说明CVPixelBuffer只有一个访问单元(Access Unit),而BottomField和TopField两个域表达了图像缓冲区两个色度的位置,与图像倒转无关。其余参数,如YCbCrMatrix只是源视频需要的YUV转RGB矩阵。
所以,根据我对Core Video的了解,目前使用Core Video接口无法处理此情况,只能在GPU中通过镜像纹理坐标或者使用前面介绍的垂直镜像方式解决。
参考与推荐阅读
文/熊皮皮(简书作者)
原文链接:http://www.jianshu.com/p/dac9857b34d0
Video Toolbox:读写解码回调函数CVImageBufferRef的YUV图像的更多相关文章
- iOS面向编码|iOSVideoToolbox:读写解码回调函数CVImageBufferRef的YUV图像
iOS面向编码|iOSVideoToolbox:读写解码回调函数CVImageBufferRef的YUV图像 本文档基于H.264的解码,介绍读写Video Toolbox解码回调函数参数CVImag ...
- nodejs基础(回调函数、模块、事件、文件读写、目录的创建与删除)
node官网:http://nodejs.cn/ 今天想看看node的视频,对node进一步了解, 1.我们可以从官网下载node到自己的电脑上,今天了解到node的真正概念,node时javascr ...
- C++中回调函数(CallBack)的使用
如果试图直接使用C++的成员函数作为回调函数将发生错误,甚至编译就不能通过. 其错误是普通的C++成员函数都隐含了一个传递函数作为参数,亦即“this”指针,C++通过传递this指针给其成员函数从而 ...
- (60)Wangdao.com第十天_JavaScript 函数_作用域_闭包_IIFE_回调函数_eval
函数 实现特定功能的 n 条语句封装体. 1. 创建一个函数对象 var myFunc = new Function(); // typeof myFunc 将会打印 function ...
- Javascript异步编程之二回调函数
上一节讲异步原理的时候基本上把回掉函数也捎带讲了一些,这节主要举几个例子来具体化一下.在开始之前,首先要明白一件事,在javascript里函数可以作为参数进行传递,这里涉及到高阶函数的概念,大家可以 ...
- 第三十四天- 线程队列、线程池(map/submit/shutdown/回调函数)
1.线程列队 queue队列 :使用import queue,用法与进程Queue一样 class queue.Queue(maxsize=0) # 先进先出: q = queue.Queue(3) ...
- Node.js:创建应用+回调函数(阻塞/非阻塞)+事件循环
一.创建应用 如果我们使用PHP来编写后端的代码时,需要Apache 或者 Nginx 的HTTP 服务器,并配上 mod_php5 模块和php-cgi.从这个角度看,整个"接收 HTTP ...
- Node js 安装+回调函数+事件
/* 从网站 https://nodejs.org/zh-cn/ 下载 这里用的 9.4.0 版本 下载完安装 安装目录是 D:\ApacheServer\node 一路默认安装 安装后打开cmd命令 ...
- 关于跨进程使用回调函数的研究:以跨进程获取Richedit中RTF流为例(在Delphi 初始化每一个TWinControl 对象时,将会在窗体 的属性(PropData)中加入一些标志,DLL的HInstance的值与HOST 进程的HInstance并不一致)
建议先参考我上次写的博文跨进程获取Richedit中Text: 获得QQ聊天输入框中的内容 拿到这个问题,我习惯性地会从VCL内核开始分析.找到TRichEdit声明的单元,分析TRichEdit保存 ...
随机推荐
- Table of Contents - JMS
JMS Specification v1.1 JMS 基本概念 Message QueueBrowser 消息选择器 消息确认 ConnectionMetaData ExceptionListener ...
- 根据不同的浏览器对不同元素进行css调整
<!if firefox> .element { top:4px; } <![endif]> <!if chrome> .element { top:6px; } ...
- php数组编码转换函数的示例
场景说明/问题描述: Ajax提交页面编码为gb2312,数据库编码为utf8,在不更改页面及数据库编码的情况下插入数据. 自定义函数: 代码如下 复制代码 function array_iconv ...
- eBay 开发流程
1[记录]注册成为eBay开发者(eBay Developers Program)+创建Sanbox Key和Production Key http://www.crifan.com/register ...
- 根据DateTime来获取当天是周几(已完结)
只需要以下代码: @System.Globalization.CultureInfo.CurrentCulture.DateTimeFormat.GetDayName(item.CreateTime. ...
- c# list排序
List<int> tmp = new List<int>(){5,1,22,11,4}; 升序:tmp.Sort((x, y) => x.CompareTo(y)); ...
- C# 线程--第一单线程基础
概念 什么是进程? 当一个程序被打开运行时,它就是一个进程.在进程中包括线程,进程可以由一个或多个线程组成. 什么是线程? 线程是程序执行流的最小单元.一个标准的线程由线程ID,当前指令指针(PC), ...
- (转)分布式缓存GemFire架构介绍
1什么是GemFire GemFire是一个位于应用集群和后端数据源之间的高性能.分布式的操作数据(operational data)管理基础架构.它提供了低延迟.高吞吐量的数据共享和事件分发.Gem ...
- java非静态变量初始化
java费静态变量的初始化分为两种情况,一种是局部变量,一种是类的域. 对于类的域,java在类初始化时,会为变量赋一个初始值.对于基本数据类型,java会将初始值设置为二进制0,具体为将boolea ...
- OOA、OOD、OOP
复习 OOA.OOD.OOP OOA Object-Oriented Analysis:面向对象分析方法 是在一个系统的开发过程中进行了系统业务调查以后,按照面向对象的思想来分析问题.OOA与结构 ...