OpenAL播放pcm或wav数据流-windows/iOS/Android(一)

 

最近在研究渲染问题,本文采用openal做pcm和wav数据流播放,并非本地文件,demo是windows的,ios通用。网上都是ios的,ios需要引用OpenAl.framework框架,

Android平台需要做openal的jni,android的openal库可以参考

http://blog.csdn.NET/matrix_laboratory/article/details/53319735这篇文章,各个平台需要做稍微处理。

下面是代码:

//.h

  1. /** Copyright (c/c++) <2016.11.22> <zwg/>
  2. * Function
  3. * OpenAL through the buffer queuing mechanism to support the streaming playback of sound. The buffer queue is a buffer associated with a single source contact mechanism.
  4. * when audio playback, continuous rendering of each buffer, as if the buffer is composed of a continuous sound. This can be controlled by some special functions.
  5. * flow is generally the source of the work. In a number of audio buffer by alSourceQueueBuffers () function to queue, and then play the sound source,
  6. * next with property AL_BUFFERS_PROCESSED to query. This property obtains the number of buffers that have been processed,
  7. * allows applications to use the alSourceUnqueueBuffers () function to delete the buffers that have been processed.
  8. * alSourceUnqueueBuffers () function will start from the queue header will be processed in order to remove the buffer. Finally, the rest of the buffer queue in gear.
  9. * Opanal for audio rendering related implementation and definition, etc.
  10. * OpenAL通过缓冲器排队机制支持声音的流式播放。缓冲器排队是多个缓冲器与单一音源相关联的一种机制。
  11. * 当音源播放时,连续对各个缓冲器进行渲染,就好象这些缓冲器组成了一个连续的声音。这可以通过一些特殊函数来控制。
  12. * 流音源的工作一般是这样的。音源里的一批缓冲器通过alSourceQueueBuffers()函数进行排队,然后播放音源,
  13. * 接下来用属性AL_BUFFERS_PROCESSED来查询。该属性得出已经处理好的缓冲器的数量,
  14. * 从而允许应用程序使用alSourceUnqueueBuffers()函数删除那些已经处理好的缓冲器。
  15. * alSourceUnqueueBuffers()函数将从队列头部开始依次将处理好的缓冲器删除。最后,其余的缓冲器在音源上排队。
  16. * OpanAl 用于音频渲染相关实现及定义,等
  17. */
  18. #ifndef __LVS_OPENAL_INTERFACE_H__
  19. #define __LVS_OPENAL_INTERFACE_H__
  20. #include <stdio.h>
  21. #include <stdlib.h>
  22. #include <string>
  23. //windows
  24. #ifdef WIN32
  25. #include <Windows.h>
  26. //openAl库
  27. #include "alut.h"
  28. #pragma comment(lib,"alut.lib")
  29. #pragma comment(lib,"OpenAL32.lib")
  30. //ios
  31. #elif __APPLE__
  32. #include "alut.h"
  33. //ANDROID平台
  34. #elif __ANDROID__
  35. #include "alut.h"
  36. //linux
  37. #else
  38. #include "alut.h"
  39. #endif
  40. //到处宏定义
  41. //windows
  42. #ifdef WIN32
  43. #define LVS_DLLEXPORT __declspec(dllexport)
  44. //ios
  45. #elif __APPLE__
  46. #define LVS_DLLEXPORT
  47. //linux
  48. #else
  49. #define LVS_DLLEXPORT
  50. #endif
  51. using namespace std;
  52. //接口初始化
  53. int lvs_openal_interface_init();
  54. //接口释放
  55. void lvs_openal_interface_uninit();
  56. //接口开始播放
  57. void lvs_openal_interface_playsound();
  58. //接口停止播放
  59. void lvs_openal_interface_stopsound();
  60. //接口设置音量
  61. void lvs_openal_interface_setvolume(float volume);//volume取值范围(0~1)
  62. //接口获取音量
  63. float lvs_openal_interface_getvolume();
  64. //接口传入pcm数据用于播放
  65. int lvs_openal_interface_openaudiofromqueue(char* data,int dataSize,int aSampleRate,int aBit ,int aChannel);
  66. //更新队列数据,删除已经播放的buffer,这个在队列满的时候用
  67. int lvs_openal_interface_updataQueueBuffer();
  68. //获取当前时间戳
  69. long long lvs_openal_interface_getrealpts();
  70. //获取已经播放了多少个数据块
  71. long long lvs_openal_interface_getIsplayBufferSize();
  72. //获取缓存队列长度
  73. int lvs_openal_getnumqueuedsize();
  74. class cclass_openal_interface;
  75. class cclass_openal_interface
  76. {
  77. public:
  78. cclass_openal_interface();
  79. virtual ~cclass_openal_interface();
  80. //开始播放
  81. void playSound();
  82. //停止播放
  83. void stopSound();
  84. //设置音量
  85. void SetVolume(float volume);//volume取值范围(0~1)
  86. //获取音量
  87. float GetVolume();
  88. //传入pcm数据用于播放
  89. int openAudioFromQueue(char* data,int dataSize,int aSampleRate,int aBit ,int aChannel);
  90. //更新队列数据,删除已经播放的buffer
  91. int updataQueueBuffer();
  92. private:
  93. //初始化openal
  94. int initOpenAL();
  95. //释放openal
  96. void cleanUpOpenAL();
  97. public:
  98. int m_numprocessed;             //队列中已经播放过的数量
  99. int m_numqueued;                //队列中缓冲队列数量
  100. long long m_IsplayBufferSize;   //已经播放了多少个音频缓存数目
  101. double m_oneframeduration;      //一帧音频数据持续时间(ms)
  102. float m_volume;                 //当前音量volume取值范围(0~1)
  103. int m_samplerate;               //采样率
  104. int m_bit;                      //样本值
  105. int m_channel;                  //声道数
  106. int m_datasize;                 //一帧音频数据量
  107. private:
  108. ALCdevice * m_Devicde;          //device句柄
  109. ALCcontext * m_Context;         //device context
  110. ALuint m_outSourceId;           //source id 负责播放
  111. };
  112. #endif

//.cpp

  1. #include "Lvs_OpenAl_Interface.h"
  2. static cclass_openal_interface * copenal_interface = NULL;
  3. int lvs_openal_interface_init()
  4. {
  5. int ret = 0;
  6. printf("Device : lvs_openal_interface_init\n");
  7. if(copenal_interface == NULL)
  8. {
  9. copenal_interface = new cclass_openal_interface();
  10. }
  11. return ret;
  12. }
  13. void lvs_openal_interface_uninit()
  14. {
  15. printf("Device : lvs_openal_interface_uninit\n");
  16. if(copenal_interface)
  17. {
  18. delete copenal_interface;
  19. copenal_interface = NULL;
  20. }
  21. return ;
  22. }
  23. void lvs_openal_interface_playsound()
  24. {
  25. copenal_interface->playSound();
  26. }
  27. void lvs_openal_interface_stopsound()
  28. {
  29. copenal_interface->stopSound();
  30. }
  31. void lvs_openal_interface_setvolume(float volume)//volume取值范围(0~1)
  32. {
  33. copenal_interface->SetVolume(volume);
  34. }
  35. float lvs_openal_interface_getvolume()
  36. {
  37. return copenal_interface->GetVolume();
  38. }
  39. int lvs_openal_interface_openaudiofromqueue(char* data,int dataSize,int aSampleRate,int aBit ,int aChannel)
  40. {
  41. return copenal_interface->openAudioFromQueue(data,dataSize,aSampleRate,aBit,aChannel);
  42. }
  43. long long lvs_openal_interface_getrealpts()
  44. {
  45. long long time = (long long )((copenal_interface->m_IsplayBufferSize * copenal_interface->m_oneframeduration) + 0.5);
  46. printf("*****m_IsplayBufferSize : %ld",copenal_interface->m_IsplayBufferSize);
  47. printf("****************time : %lld(ms)\n",time);
  48. return time;
  49. }
  50. long long lvs_openal_interface_getIsplayBufferSize()
  51. {
  52. return copenal_interface->m_IsplayBufferSize;
  53. }
  54. int lvs_openal_getnumqueuedsize()
  55. {
  56. return copenal_interface->m_numqueued;
  57. }
  58. int lvs_openal_interface_updataQueueBuffer()
  59. {
  60. return copenal_interface->updataQueueBuffer();
  61. }
  62. cclass_openal_interface::cclass_openal_interface()
  63. {
  64. m_Devicde = NULL;
  65. m_Context = NULL;
  66. m_outSourceId = 0;
  67. m_numprocessed = 0;
  68. m_numqueued = 0;
  69. m_IsplayBufferSize = 0;
  70. m_oneframeduration = 0.0;
  71. m_volume = 1.0;
  72. m_samplerate = 0;
  73. m_bit = 0;
  74. m_channel = 0;
  75. m_datasize = 0;
  76. //init
  77. initOpenAL();
  78. }
  79. cclass_openal_interface::~cclass_openal_interface()
  80. {
  81. cleanUpOpenAL();
  82. m_Devicde = NULL;
  83. m_Context = NULL;
  84. m_outSourceId = 0;
  85. m_numprocessed = 0;
  86. m_numqueued = 0;
  87. m_IsplayBufferSize = 0;
  88. m_oneframeduration = 0.0;
  89. m_volume = 1.0;
  90. m_samplerate = 0;
  91. m_bit = 0;
  92. m_channel = 0;
  93. m_datasize = 0;
  94. }
  95. int cclass_openal_interface::initOpenAL()
  96. {
  97. int ret = 0;
  98. printf("=======initOpenAl===\n");
  99. #ifdef WIN32
  100. //初始化 ALUT openal函数库
  101. int zwg_argc=1;
  102. //添加函数库名称
  103. char* zwg_argv[]={"ZWG_ALUT"};
  104. ret= alutInit(&zwg_argc, zwg_argv);
  105. #else
  106. #endif
  107. //打开device
  108. m_Devicde = alcOpenDevice(NULL);
  109. if (m_Devicde)
  110. {
  111. #ifdef WIN32
  112. //windows 用这个context 声音不正常,以后处理
  113. #else
  114. //建立声音文本描述
  115. m_Context = alcCreateContext(m_Devicde, NULL);
  116. //设置行为文本描述
  117. alcMakeContextCurrent(m_Context);
  118. #endif
  119. }
  120. //创建一个source并设置一些属性
  121. alGenSources(1, &m_outSourceId);
  122. alSpeedOfSound(1.0);
  123. alDopplerVelocity(1.0);
  124. alDopplerFactor(1.0);
  125. alSourcef(m_outSourceId, AL_PITCH, 1.0f);
  126. alSourcef(m_outSourceId, AL_GAIN, 1.0f);
  127. alSourcei(m_outSourceId, AL_LOOPING, AL_FALSE);
  128. alSourcef(m_outSourceId, AL_SOURCE_TYPE, AL_STREAMING);
  129. return ret;
  130. }
  131. void cclass_openal_interface::cleanUpOpenAL()
  132. {
  133. printf("=======cleanUpOpenAL===\n");
  134. alDeleteSources(1, &m_outSourceId);
  135. #ifdef WIN32
  136. alcCloseDevice(m_Devicde);
  137. m_Devicde = NULL;
  138. alutExit();
  139. #else
  140. ALCcontext * Context = alcGetCurrentContext();
  141. ALCdevice * Devicde = alcGetContextsDevice(Context);
  142. if (Context)
  143. {
  144. alcMakeContextCurrent(NULL);
  145. alcDestroyContext(Context);
  146. m_Context = NULL;
  147. }
  148. alcCloseDevice(m_Devicde);
  149. m_Devicde = NULL;
  150. #endif
  151. }
  152. void cclass_openal_interface::playSound()
  153. {
  154. int ret = 0;
  155. alSourcePlay(m_outSourceId);
  156. if((ret = alGetError()) != AL_NO_ERROR)
  157. {
  158. printf("error alcMakeContextCurrent %x : %s\n", ret,alutGetErrorString (ret));
  159. }
  160. }
  161. void cclass_openal_interface::stopSound()
  162. {
  163. alSourceStop(m_outSourceId);
  164. }
  165. void cclass_openal_interface::SetVolume(float volume)//volume取值范围(0~1)
  166. {
  167. m_volume = volume;
  168. alSourcef(m_outSourceId,AL_GAIN,volume);
  169. }
  170. float cclass_openal_interface::GetVolume()
  171. {
  172. return m_volume;
  173. }
  174. int cclass_openal_interface::updataQueueBuffer()
  175. {
  176. //播放状态字段
  177. ALint stateVaue = 0;
  178. //获取处理队列,得出已经播放过的缓冲器的数量
  179. alGetSourcei(m_outSourceId, AL_BUFFERS_PROCESSED, &m_numprocessed);
  180. //获取缓存队列,缓存的队列数量
  181. alGetSourcei(m_outSourceId, AL_BUFFERS_QUEUED, &m_numqueued);
  182. //获取播放状态,是不是正在播放
  183. alGetSourcei(m_outSourceId, AL_SOURCE_STATE, &stateVaue);
  184. //printf("===statevaue ========================%x\n",stateVaue);
  185. if (stateVaue == AL_STOPPED ||
  186. stateVaue == AL_PAUSED ||
  187. stateVaue == AL_INITIAL)
  188. {
  189. //如果没有数据,或数据播放完了
  190. if (m_numqueued < m_numprocessed || m_numqueued == 0 ||(m_numqueued == 1 && m_numprocessed ==1))
  191. {
  192. //停止播放
  193. printf("...Audio Stop\n");
  194. stopSound();
  195. cleanUpOpenAL();
  196. return 0;
  197. }
  198. if (stateVaue != AL_PLAYING)
  199. {
  200. playSound();
  201. }
  202. }
  203. //将已经播放过的的数据删除掉
  204. while(m_numprocessed --)
  205. {
  206. ALuint buff;
  207. //更新缓存buffer中的数据到source中
  208. alSourceUnqueueBuffers(m_outSourceId, 1, &buff);
  209. //删除缓存buff中的数据
  210. alDeleteBuffers(1, &buff);
  211. //得到已经播放的音频队列多少块
  212. m_IsplayBufferSize ++;
  213. }
  214. long long time = (long long )((m_IsplayBufferSize * m_oneframeduration) + 0.5);
  215. //printf("*****m_IsplayBufferSize : %ld",m_IsplayBufferSize);
  216. //printf("****************time : %ld(ms)\n",time);
  217. return 1;
  218. }
  219. int cclass_openal_interface::openAudioFromQueue(char* data,int dataSize,int aSampleRate,int aBit ,int aChannel)
  220. {
  221. int ret = 0;
  222. //样本数openal的表示方法
  223. ALenum format = 0;
  224. //buffer id 负责缓存,要用局部变量每次数据都是新的地址
  225. ALuint bufferID = 0;
  226. if (m_datasize == 0 &&
  227. m_samplerate == 0 &&
  228. m_bit == 0 &&
  229. m_channel == 0)
  230. {
  231. if (dataSize != 0 &&
  232. aSampleRate != 0 &&
  233. aBit != 0 &&
  234. aChannel != 0)
  235. {
  236. m_datasize = dataSize;
  237. m_samplerate = aSampleRate;
  238. m_bit = aBit;
  239. m_channel = aChannel;
  240. m_oneframeduration = m_datasize * 1.0 /(m_bit/8) /m_channel /m_samplerate * 1000 ;   //计算一帧数据持续时间
  241. }
  242. }
  243. //创建一个buffer
  244. alGenBuffers(1, &bufferID);
  245. if((ret = alGetError()) != AL_NO_ERROR)
  246. {
  247. printf("error alGenBuffers %x : %s\n", ret,alutGetErrorString (ret));
  248. //AL_ILLEGAL_ENUM
  249. //AL_INVALID_VALUE
  250. //#define AL_ILLEGAL_COMMAND                        0xA004
  251. //#define AL_INVALID_OPERATION                      0xA004
  252. }
  253. if (aBit == 8)
  254. {
  255. if (aChannel == 1)
  256. {
  257. format = AL_FORMAT_MONO8;
  258. }
  259. else if(aChannel == 2)
  260. {
  261. format = AL_FORMAT_STEREO8;
  262. }
  263. }
  264. if( aBit == 16 )
  265. {
  266. if( aChannel == 1 )
  267. {
  268. format = AL_FORMAT_MONO16;
  269. }
  270. if( aChannel == 2 )
  271. {
  272. format = AL_FORMAT_STEREO16;
  273. }
  274. }
  275. //指定要将数据复制到缓冲区中的数据
  276. alBufferData(bufferID, format, data, dataSize,aSampleRate);
  277. if((ret = alGetError()) != AL_NO_ERROR)
  278. {
  279. printf("error alBufferData %x : %s\n", ret,alutGetErrorString (ret));
  280. //AL_ILLEGAL_ENUM
  281. //AL_INVALID_VALUE
  282. //#define AL_ILLEGAL_COMMAND                        0xA004
  283. //#define AL_INVALID_OPERATION                      0xA004
  284. }
  285. //附加一个或一组buffer到一个source上
  286. alSourceQueueBuffers(m_outSourceId, 1, &bufferID);
  287. if((ret = alGetError()) != AL_NO_ERROR)
  288. {
  289. printf("error alSourceQueueBuffers %x : %s\n", ret,alutGetErrorString (ret));
  290. }
  291. //更新队列数据
  292. ret = updataQueueBuffer();
  293. //删除一个缓冲 这里不应该删除缓冲,在source里面播放完毕删除
  294. //alDeleteBuffers(1, &bufferID);
  295. bufferID = 0;
  296. return 1;
  297. }

//main.cpp

  1. #include "Lvs_OpenAl_Interface.h"
  2. //要显示的pcm/wav文件路径及名称
  3. #define PCM_STREAM_PATH_NAME  "../pcm_stream/44100_2_16.pcm"
  4. int main()
  5. {
  6. int ret = 0;
  7. int nSampleRate = 44100;                   //采样率
  8. int nBit = 16;                             //样本数
  9. int nChannel = 2;                          //声道
  10. int ndatasize = 1024 * (nBit/8) *nChannel; //每次读取的数据大小
  11. char ndata[4096 + 1] = {0};                //读取的数据
  12. FILE * pFile_pcm = NULL;                   //读取pcm数据的文件句柄
  13. //打开pcm文件
  14. if((pFile_pcm = fopen(PCM_STREAM_PATH_NAME, "rb")) == NULL)
  15. {
  16. printf("filed open file : %s\n",PCM_STREAM_PATH_NAME);
  17. return getchar();
  18. }
  19. else
  20. {
  21. printf("success open file : %s\n",PCM_STREAM_PATH_NAME);
  22. }
  23. //init
  24. lvs_openal_interface_init();
  25. //设置音量volume取值范围(0~1)
  26. lvs_openal_interface_setvolume(1.0);
  27. for(;;)
  28. {
  29. Sleep(23);
  30. //循环读取文件
  31. ret = fread(ndata, 1,ndatasize, pFile_pcm);
  32. if (ret != ndatasize)
  33. {
  34. //seek到文件开头
  35. fseek(pFile_pcm, 0, SEEK_SET);
  36. fread(ndata, 1,ndatasize, pFile_pcm);
  37. }
  38. //具体的处理在这里
  39. ret = lvs_openal_interface_openaudiofromqueue((char *)ndata,ndatasize,nSampleRate,nBit,nChannel);
  40. long long time = lvs_openal_interface_getrealpts();
  41. }
  42. //uinit
  43. lvs_openal_interface_uninit();
  44. //关闭pcm文件
  45. if (pFile_pcm != NULL)
  46. {
  47. fclose(pFile_pcm);
  48. pFile_pcm = NULL;
  49. }
  50. return 1;
  51. }

程序运行效果并能听到声音:

本demo还需完善。

from:http://blog.csdn.net/zhuweigangzwg/article/details/53286945

OpenAL播放pcm或wav数据流-windows/ios/android(一)的更多相关文章

  1. js判断操作系统windows,ios,android(笔记)

    使用JS判断用户使用的系统是利用浏览器的userAgent. navigator.userAgent:userAgent 获取了浏览器用于 HTTP 请求的用户代理头的值. navigator.pla ...

  2. 微软云平台媒体服务实践系列 2- 使用动态封装为iOS, Android , Windows 等多平台提供视频点播(VoD)方案

    文章微软云平台媒体服务实践系列 1- 使用静态封装为iOS, Android 设备实现点播(VoD)方案  介绍了如何针对少数iOS, Android 客户端的场景,出于节约成本的目的使用媒体服务的静 ...

  3. WIN32下使用DirectSound接口的简单音频播放器(支持wav和mp3)

    刚好最近接触了一些DirectSound,就写了一个小程序练练手,可以用来添加播放基本的wav和mp3音频文件的播放器.界面只是简单的GDI,dxsdk只使用了DirectSound8相关的接口. D ...

  4. DirectSound播放PCM(可播放实时采集的音频数据)

    前言 该篇整理的原始来源为http://blog.csdn.net/leixiaohua1020/article/details/40540147.非常感谢该博主的无私奉献,写了不少关于不同多媒体库的 ...

  5. Web Api 中使用 PCM TO WAV 的语音操作

    /// <summary> /// 语音[文件.上传.解码.保存(WAV)] /// </summary> [DeveloperEx("Liwei:秘书语音需求单&q ...

  6. 最简单的视音频播放示例9:SDL2播放PCM

    本文记录SDL播放音频的技术.在这里使用的版本是SDL2.实际上SDL本身并不提供视音频播放的功能,它只是封装了视音频播放的底层API.在Windows平台下,SDL封装了Direct3D这类的API ...

  7. 最简单的视音频播放示例8:DirectSound播放PCM

    本文记录DirectSound播放音频的技术.DirectSound是Windows下最常见的音频播放技术.目前大部分的音频播放应用都是通过DirectSound来播放的.本文记录一个使用Direct ...

  8. 最简单的视音频播放演示样例8:DirectSound播放PCM

    ===================================================== 最简单的视音频播放演示样例系列文章列表: 最简单的视音频播放演示样例1:总述 最简单的视音频 ...

  9. 微信小程序语音与讯飞语音识别接口(Java),Kronopath/SILKCodec,ffmpeg处理silk,pcm,wav转换

    项目需求,需要使用讯飞的语音识别接口,将微信小程序上传的录音文件识别成文字返回 首先去讯飞开放平台中申请开通语音识别功能 在这里面下载sdk,然后解压,注意appid与sdk是关联的,appid在初始 ...

随机推荐

  1. 我自己曾经经历的CMMI3认证通过关于软件测试的访谈【转载】

    转自<http://blog.csdn.net/jcy58/article/details/51908884> 因为当初我在公司里是负责软件测试工作的,所以CMMI3和测试相关的访谈,就是 ...

  2. 九度OJ 1197:奇偶校验 (基础题)

    时间限制:1 秒 内存限制:32 兆 特殊判题:否 提交:3590 解决:1511 题目描述: 输入一个字符串,然后对每个字符进行奇校验,最后输出校验后的二进制数(如'3',输出:10110011). ...

  3. iOS NSError HTTP错误码大全

    NSError codes in the Cocoa error domain. enum { NSFileNoSuchFileError = 4, NSFileLockingError = 255, ...

  4. JVM性能优化, Part 5 Java的伸缩性

    很多程序员在解决JVM性能问题的时候,花开了很多时间去调优应用程序级别的性能瓶颈,当你读完这本系列文章之后你会发现我可能更加系统地看待这类的问题.我说过JVM的自身技术限制了Java企业级应用的伸缩性 ...

  5. selenium java 封装

    1.简单介绍 1)展示如何封装selenium的api,使其符合我们的使用需求: 2)展示如何使用page object模式写selenium脚本: 3)展示如何即时查找元素,用以操作ajax页面: ...

  6. Python:笔记(2)——函数与模块

    Python:笔记(2)——函数与模块 Python函数 关于函数 1.我们可以使用Help来查看函数的帮助信息 2.调用函数的时候,如果传入的参数数量或者类型不符合均会报错. 3.函数名其实就是一个 ...

  7. CSS选择器(一)

    一.CSS 元素选择器 最常见的 CSS 选择器是元素选择器.换句话说,文档的元素就是最基本的选择器. 如果设置 HTML 的样式,选择器通常将是某个 HTML 元素,比如 p.h1.em.a,甚至可 ...

  8. BOM之history

    history是JavaScript中BOM上的一个对象,其中存储了浏览器的历史记录 history存储简单过程 浏览器会将一个窗口中访问的网页进行记录,不管我们通过以下哪种方式改变页面,浏览器都会把 ...

  9. hd acm2035

    求A^B的最后三位数表示的整数.说明:A^B的含义是“A的B次方” 思路:后三位只跟后三位相乘有关,所以可以每乘一次都对1000取余. 代码: #include <stdio.h>#inc ...

  10. Entity Framework中的Identity map和Unit of Work模式(转)

    一,什么是Identity map模式 Identity map是EF获取和缓存数据的模式.Identity map模式指的是任何数据都只会被加载一次,以map的形式缓存,以唯一的identity来再 ...