Android音频(四)AudioTrack

原址

播放声音可以用MediaPlayer和AudioTrack,两者都提供了Java API供应用开发者使用。虽然都可以播放声音,但两者还是有很大的区别的。其中最大的区别是MediaPlayer可以播放多种格式的声音文件,例如MP3,AAC,WAV,OGG,MIDI等。MediaPlayer会在framework层创建对应的音频解码器。而AudioTrack只能播放已经解码的PCM流,如果是文件的话只支持wav格式的音频文件,因为wav格式的音频文件大部分都是PCM流。AudioTrack不创建解码器,所以只能播放不需要解码的wav文件。当然两者之间还是有紧密的联系,MediaPlayer在framework层还是会创建AudioTrack,把解码后的PCM数流传递给AudioTrack,AudioTrack再传递给AudioFlinger进行混音,然后才传递给硬件播放,所以是MediaPlayer包含了AudioTrack。使用AudioTrack播放音乐示例:

[java]  view plain  copy
  1. AudioTrack audio = new AudioTrack(  
  2.      AudioManager.STREAM_MUSIC, // 指定流的类型  
  3.      32000// 设置音频数据的采样率 32k,如果是44.1k就是44100  
  4.      AudioFormat.CHANNEL_OUT_STEREO, // 设置输出声道为双声道立体声,而CHANNEL_OUT_MONO类型是单声道  
  5.      AudioFormat.ENCODING_PCM_16BIT, // 设置音频数据块是8位还是16位,这里设置为16位。好像现在绝大多数的音频都是16位的了  
  6.      AudioTrack.MODE_STREAM // 设置模式类型,在这里设置为流类型,另外一种MODE_STATIC貌似没有什么效果  
  7.      );  
  8. audio.play(); // 启动音频设备,下面就可以真正开始音频数据的播放了  
  9. // 打开mp3文件,读取数据,解码等操作省略 ...  
  10. byte[] buffer = new buffer[4096];  
  11. int count;  
  12. while(true)  
  13. {  
  14.     // 最关键的是将解码后的数据,从缓冲区写入到AudioTrack对象中  
  15.     audio.write(buffer, 04096);  
  16.     if(文件结束) break;  
  17. }  
  18. //关闭并释放资源  
  19. audio.stop();  
  20. audio.release();  

AudioTrack构造过程

每一个音频流对应着一个AudioTrack类的一个实例,每个AudioTrack会在创建时注册到 AudioFlinger中,由AudioFlinger把所有的AudioTrack进行混合(Mixer),然后输送到AudioHardware中进行播放,目前Android同时最多可以创建32个音频流,也就是说,Mixer最多会同时处理32个AudioTrack的数据流。

frameworks\base\media\Java\android\media\AudioTrack.java

[java]  view plain  copy
  1. /** 
  2.  * streamType:音频流类型 
  3.  * sampleRateInHz:采样率 
  4.  * channelConfig:音频声道 
  5.  * audioFormat:音频格式 
  6.  * bufferSizeInBytes缓冲区大小: 
  7.  * mode:音频数据加载模式 
  8.  * sessionId:会话id 
  9.  */  
  10. public AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat,  
  11.         int bufferSizeInBytes, int mode, int sessionId)  
  12. throws IllegalArgumentException {  
  13.     // mState already == STATE_UNINITIALIZED  
  14.   
  15.     // remember which looper is associated with the AudioTrack instantiation  
  16.     Looper looper;  
  17.     if ((looper = Looper.myLooper()) == null) {  
  18.         looper = Looper.getMainLooper();  
  19.     }  
  20.     mInitializationLooper = looper;  
  21.     /** 
  22.      * 参数检查 
  23.      * 1.检查streamType是否为:STREAM_ALARM、STREAM_MUSIC、STREAM_RING、STREAM_SYSTEM、STREAM_VOICE_CALL、 
  24.      *  STREAM_NOTIFICATION、STREAM_BLUETOOTH_SCO、STREAM_BLUETOOTH_SCO,并赋值给mStreamType 
  25.      * 2.检查sampleRateInHz是否在4000到48000之间,并赋值给mSampleRate 
  26.      * 3.设置mChannels:  
  27.      *      CHANNEL_OUT_DEFAULT、CHANNEL_OUT_MONO、CHANNEL_CONFIGURATION_MONO ---> CHANNEL_OUT_MONO 
  28.      *      CHANNEL_OUT_STEREO、CHANNEL_CONFIGURATION_STEREO                  ---> CHANNEL_OUT_STEREO 
  29.      * 4.设置mAudioFormat:  
  30.      *      ENCODING_PCM_16BIT、ENCODING_DEFAULT ---> ENCODING_PCM_16BIT 
  31.      *      ENCODING_PCM_8BIT ---> ENCODING_PCM_8BIT 
  32.      * 5.设置mDataLoadMode: 
  33.      *      MODE_STREAM 
  34.      *      MODE_STATIC 
  35.      */  
  36.     audioParamCheck(streamType, sampleRateInHz, channelConfig, audioFormat, mode);  
  37.     /** 
  38.      * buffer大小检查,计算每帧字节大小,如果是ENCODING_PCM_16BIT,则为mChannelCount * 2 
  39.      * mNativeBufferSizeInFrames为帧数 
  40.      */  
  41.     audioBuffSizeCheck(bufferSizeInBytes);  
  42.     if (sessionId < 0) {  
  43.         throw new IllegalArgumentException("Invalid audio session ID: "+sessionId);  
  44.     }  
  45.     //进入native层初始化  
  46.     int[] session = new int[1];  
  47.     session[0] = sessionId;  
  48.     // native initialization  
  49.     int initResult = native_setup(new WeakReference<AudioTrack>(this),  
  50.             mStreamType, mSampleRate, mChannels, mAudioFormat,  
  51.             mNativeBufferSizeInBytes, mDataLoadMode, session);  
  52.     if (initResult != SUCCESS) {  
  53.         loge("Error code "+initResult+" when initializing AudioTrack.");  
  54.         return// with mState == STATE_UNINITIALIZED  
  55.     }  
  56.     mSessionId = session[0];  
  57.     if (mDataLoadMode == MODE_STATIC) {  
  58.         mState = STATE_NO_STATIC_DATA;  
  59.     } else {  
  60.         mState = STATE_INITIALIZED;  
  61.     }  
  62. }  

with audio session. Use this constructor when the AudioTrack must be attached to a particular audio session. The primary use of the audio session ID is to associate audio effects to a particular instance of AudioTrack: if an audio session ID is provided when creating an AudioEffect, this effect will be applied only to audio tracks and media players in the same session and not to the output mix. When an AudioTrack is created without specifying a session, it will create its own session which can be retreived by calling the getAudioSessionId() method. If a non-zero session ID is provided, this AudioTrack will share effects attached to this session with all other media players or audio tracks in the same session, otherwise a new session will be created for this track if none is supplied.

streamType

the type of the audio stream. See STREAM_VOICE_CALL,STREAM_SYSTEM,STREAM_RING,STREAM_MUSIC,STREAM_ALARM, andSTREAM_NOTIFICATION.

sampleRateInHz

the sample rate expressed in Hertz.

channelConfig

describes the configuration of the audio channels. SeeCHANNEL_OUT_MONO andCHANNEL_OUT_STEREO

audioFormat

the format in which the audio data is represented. SeeENCODING_PCM_16BIT andENCODING_PCM_8BIT

bufferSizeInBytes

the total size (in bytes) of the buffer where audio data is read from for playback. If using the AudioTrack in streaming mode, you can write data into this buffer in smaller chunks than this size. If using the AudioTrack in static mode, this is the maximum size of the sound that will be played for this instance. SeegetMinBufferSize(int, int, int) to determine the minimum required buffer size for the successful creation of an AudioTrack instance in streaming mode. Using values smaller than getMinBufferSize() will result in an initialization failure.

mode

streaming or static buffer. See MODE_STATIC andMODE_STREAM

sessionId

Id of audio session the AudioTrack must be attached to

AudioTrack有两种数据加载模式:

  1. MODE_STREAM

在这种模式下,应用程序持续地write音频数据流到AudioTrack中,并且write动作将阻塞直到数据流从Java层传输到native层,同时加入到播放队列中。这种模式适用于播放大音频数据,但该模式也造成了一定的延时;

  1. MODE_STATIC

在播放之前,先把所有数据一次性write到AudioTrack的内部缓冲区中。适用于播放内存占用小、延时要求较高的音频数据。

frameworks\base\core\jni\android_media_AudioTrack.cpp

[cpp]  view plain  copy
  1. static int android_media_AudioTrack_native_setup(JNIEnv *env, jobject thiz, jobject weak_this,jint streamType, jint sampleRateInHertz, jint javaChannelMask,  
  2.         jint audioFormat, jint buffSizeInBytes, jint memoryMode, jintArray jSession)  
  3. {  
  4.     ALOGV("sampleRate=%d, audioFormat(from Java)=%d, channel mask=%x, buffSize=%d",  
  5.         sampleRateInHertz, audioFormat, javaChannelMask, buffSizeInBytes);  
  6.     int afSampleRate;//采样率  
  7.     int afFrameCount;//帧数  
  8.     //通过AudioSystem从AudioPolicyService中读取对应音频流类型的帧数  
  9.     if (AudioSystem::getOutputFrameCount(&afFrameCount, (audio_stream_type_t) streamType) != NO_ERROR) {  
  10.         ALOGE("Error creating AudioTrack: Could not get AudioSystem frame count.");  
  11.         return AUDIOTRACK_ERROR_SETUP_AUDIOSYSTEM;  
  12.     }  
  13.     //通过AudioSystem从AudioPolicyService中读取对应音频流类型的采样率  
  14.     if (AudioSystem::getOutputSamplingRate(&afSampleRate, (audio_stream_type_t) streamType) != NO_ERROR) {  
  15.         ALOGE("Error creating AudioTrack: Could not get AudioSystem sampling rate.");  
  16.         return AUDIOTRACK_ERROR_SETUP_AUDIOSYSTEM;  
  17.     }  
  18.     // Java channel masks don't map directly to the native definition, but it's a simple shift  
  19.     // to skip the two deprecated channel configurations "default" and "mono".  
  20.     uint32_t nativeChannelMask = ((uint32_t)javaChannelMask) >> 2;  
  21.     //判断是否为输出通道  
  22.     if (!audio_is_output_channel(nativeChannelMask)) {  
  23.         ALOGE("Error creating AudioTrack: invalid channel mask.");  
  24.         return AUDIOTRACK_ERROR_SETUP_INVALIDCHANNELMASK;  
  25.     }  
  26.     //得到通道个数,popcount函数用于统计一个整数中有多少位为1  
  27.     int nbChannels = popcount(nativeChannelMask);  
  28.     // check the stream type  
  29.     audio_stream_type_t atStreamType;  
  30.     switch (streamType) {  
  31.     case AUDIO_STREAM_VOICE_CALL:  
  32.     case AUDIO_STREAM_SYSTEM:  
  33.     case AUDIO_STREAM_RING:  
  34.     case AUDIO_STREAM_MUSIC:  
  35.     case AUDIO_STREAM_ALARM:  
  36.     case AUDIO_STREAM_NOTIFICATION:  
  37.     case AUDIO_STREAM_BLUETOOTH_SCO:  
  38.     case AUDIO_STREAM_DTMF:  
  39.         atStreamType = (audio_stream_type_t) streamType;  
  40.         break;  
  41.     default:  
  42.         ALOGE("Error creating AudioTrack: unknown stream type.");  
  43.         return AUDIOTRACK_ERROR_SETUP_INVALIDSTREAMTYPE;  
  44.     }  
  45.     // This function was called from Java, so we compare the format against the Java constants  
  46.     if ((audioFormat != javaAudioTrackFields.PCM16) && (audioFormat != javaAudioTrackFields.PCM8)) {  
  47.         ALOGE("Error creating AudioTrack: unsupported audio format.");  
  48.         return AUDIOTRACK_ERROR_SETUP_INVALIDFORMAT;  
  49.     }  
  50.     // for the moment 8bitPCM in MODE_STATIC is not supported natively in the AudioTrack C++ class so we declare everything as 16bitPCM, the 8->16bit conversion for MODE_STATIC will be handled in android_media_AudioTrack_native_write_byte()  
  51.     if ((audioFormat == javaAudioTrackFields.PCM8)  
  52.         && (memoryMode == javaAudioTrackFields.MODE_STATIC)) {  
  53.         ALOGV("android_media_AudioTrack_native_setup(): requesting MODE_STATIC for 8bit \  
  54.             buff size of %dbytes, switching to 16bit, buff size of %dbytes",  
  55.             buffSizeInBytes, 2*buffSizeInBytes);  
  56.         audioFormat = javaAudioTrackFields.PCM16;  
  57.         // we will need twice the memory to store the data  
  58.         buffSizeInBytes *= 2;  
  59.     }  
  60.     //根据不同的采样方式得到一个采样点的字节数  
  61.     int bytesPerSample = audioFormat == javaAudioTrackFields.PCM16 ? 2 : 1;  
  62.     audio_format_t format = audioFormat == javaAudioTrackFields.PCM16 ?  
  63.             AUDIO_FORMAT_PCM_16_BIT : AUDIO_FORMAT_PCM_8_BIT;  
  64.     //根据buffer大小反向计算帧数  , 一帧大小=一个采样点字节数 * 声道数  
  65.     int frameCount = buffSizeInBytes / (nbChannels * bytesPerSample);  
  66.     //判断参数的合法性  
  67.     jclass clazz = env->GetObjectClass(thiz);  
  68.     if (clazz == NULL) {  
  69.         ALOGE("Can't find %s when setting up callback.", kClassPathName);  
  70.         return AUDIOTRACK_ERROR_SETUP_NATIVEINITFAILED;  
  71.     }  
  72.     if (jSession == NULL) {  
  73.         ALOGE("Error creating AudioTrack: invalid session ID pointer");  
  74.         return AUDIOTRACK_ERROR;  
  75.     }  
  76.     jint* nSession = (jint *) env->GetPrimitiveArrayCritical(jSession, NULL);  
  77.     if (nSession == NULL) {  
  78.         ALOGE("Error creating AudioTrack: Error retrieving session id pointer");  
  79.         return AUDIOTRACK_ERROR;  
  80.     }  
  81.     int sessionId = nSession[0];  
  82.     env->ReleasePrimitiveArrayCritical(jSession, nSession, 0);  
  83.     nSession = NULL;  
  84.     // create the native AudioTrack object  
  85.     sp<AudioTrack> lpTrack = new AudioTrack();  
  86.     if (lpTrack == NULL) {  
  87.         ALOGE("Error creating uninitialized AudioTrack");  
  88.         return AUDIOTRACK_ERROR_SETUP_NATIVEINITFAILED;  
  89.     }  
  90.     // 创建存储音频数据的容器  
  91.     AudioTrackJniStorage* lpJniStorage = new AudioTrackJniStorage();  
  92.     lpJniStorage->mStreamType = atStreamType;  
  93.     //将Java层的AudioTrack引用保存到AudioTrackJniStorage中  
  94.     lpJniStorage->mCallbackData.audioTrack_class = (jclass)env->NewGlobalRef(clazz);  
  95.     // we use a weak reference so the AudioTrack object can be garbage collected.  
  96.     lpJniStorage->mCallbackData.audioTrack_ref = env->NewGlobalRef(weak_this);  
  97.     lpJniStorage->mCallbackData.busy = false;  
  98.     //初始化不同模式下的native AudioTrack对象   
  99.     if (memoryMode == javaAudioTrackFields.MODE_STREAM) { //stream模式  
  100.         lpTrack->set(   
  101.             atStreamType,// stream type  
  102.             sampleRateInHertz,  
  103.             format,// word length, PCM  
  104.             nativeChannelMask,  
  105.             frameCount,  
  106.             AUDIO_OUTPUT_FLAG_NONE,  
  107.             audioCallback,   
  108.             &(lpJniStorage->mCallbackData),//callback, callback data (user)  
  109.             0,// notificationFrames == 0 since not using EVENT_MORE_DATA to feed the AudioTrack  
  110.             0,//stream模式下的共享内存在AudioFlinger中创建  
  111.             true,// thread can call Java  
  112.             sessionId);// audio session ID  
  113.     } else if (memoryMode == javaAudioTrackFields.MODE_STATIC) {//static模式  
  114.         // 为AudioTrack分配共享内存区域   
  115.         if (!lpJniStorage->allocSharedMem(buffSizeInBytes)) {  
  116.             ALOGE("Error creating AudioTrack in static mode: error creating mem heap base");  
  117.             goto native_init_failure;  
  118.         }  
  119.         lpTrack->set(  
  120.             atStreamType,// stream type  
  121.             sampleRateInHertz,  
  122.             format,// word length, PCM  
  123.             nativeChannelMask,  
  124.             frameCount,  
  125.             AUDIO_OUTPUT_FLAG_NONE,  
  126.             audioCallback, &(lpJniStorage->mCallbackData),//callback, callback data (user));  
  127.             0,// notificationFrames == 0 since not using EVENT_MORE_DATA to feed the AudioTrack  
  128.             lpJniStorage->mMemBase,// shared mem  
  129.             true,// thread can call Java  
  130.             sessionId);// audio session ID  
  131.     }  
  132.     if (lpTrack->initCheck() != NO_ERROR) {  
  133.         ALOGE("Error initializing AudioTrack");  
  134.         goto native_init_failure;  
  135.     }  
  136.     nSession = (jint *) env->GetPrimitiveArrayCritical(jSession, NULL);  
  137.     if (nSession == NULL) {  
  138.         ALOGE("Error creating AudioTrack: Error retrieving session id pointer");  
  139.         goto native_init_failure;  
  140.     }  
  141.     // read the audio session ID back from AudioTrack in case we create a new session  
  142.     nSession[0] = lpTrack->getSessionId();  
  143.     env->ReleasePrimitiveArrayCritical(jSession, nSession, 0);  
  144.     nSession = NULL;  
  145.     {   // scope for the lock  
  146.         Mutex::Autolock l(sLock);  
  147.         sAudioTrackCallBackCookies.add(&lpJniStorage->mCallbackData);  
  148.     }  
  149.     // save our newly created C++ AudioTrack in the "nativeTrackInJavaObj" field  
  150.     // of the Java object (in mNativeTrackInJavaObj)  
  151.     setAudioTrack(env, thiz, lpTrack);  
  152.     // save the JNI resources so we can free them later  
  153.     //ALOGV("storing lpJniStorage: %x\n", (int)lpJniStorage);  
  154.     env->SetIntField(thiz, javaAudioTrackFields.jniData, (int)lpJniStorage);  
  155.     return AUDIOTRACK_SUCCESS;  
  156.     // failures:  
  157. native_init_failure:  
  158.     if (nSession != NULL) {  
  159.         env->ReleasePrimitiveArrayCritical(jSession, nSession, 0);  
  160.     }  
  161.     env->DeleteGlobalRef(lpJniStorage->mCallbackData.audioTrack_class);  
  162.     env->DeleteGlobalRef(lpJniStorage->mCallbackData.audioTrack_ref);  
  163.     delete lpJniStorage;  
  164.     env->SetIntField(thiz, javaAudioTrackFields.jniData, 0);  
  165.     return AUDIOTRACK_ERROR_SETUP_NATIVEINITFAILED;  
  166. }  

1.       检查音频参数;

2.       创建一个AudioTrack(native)对象;

3.       创建一个AudioTrackJniStorage对象;

4.       调用set函数初始化AudioTrack;

buffersize = frameCount * 每帧数据量 = frameCount * (Channel数 * 每个Channel数据量)

构造native AudioTrack

frameworks\av\media\libmedia\AudioTrack.cpp

[cpp]  view plain  copy
  1. AudioTrack::AudioTrack(): mStatus(NO_INIT),  
  2.   mIsTimed(false),  
  3.   mPreviousPriority(ANDROID_PRIORITY_NORMAL),  
  4.   mPreviousSchedulingGroup(SP_DEFAULT),  
  5.   mCblk(NULL)  
  6. {  
  7. }  

构造AudioTrackJniStorage

AudioTrackJniStorage是音频数据存储的容器,是对匿名共享内存的封装。

[cpp]  view plain  copy
  1. struct audiotrack_callback_cookie {  
  2.     jclass      audioTrack_class;  
  3.     jobject     audioTrack_ref;//Java层AudioTrack对象引用  
  4.     bool        busy;//忙判断  
  5.     Condition   cond;//互斥量  
  6. };  
  7.   
  8. class AudioTrackJniStorage {  
  9.     public:  
  10.         sp<MemoryHeapBase>         mMemHeap;  
  11.         sp<MemoryBase>             mMemBase;  
  12.         audiotrack_callback_cookie mCallbackData;  
  13.         audio_stream_type_t        mStreamType;  
  14.   
  15.     AudioTrackJniStorage() {  
  16.         mCallbackData.audioTrack_class = 0;  
  17.         mCallbackData.audioTrack_ref = 0;  
  18.         mStreamType = AUDIO_STREAM_DEFAULT;  
  19.     }  
  20.   
  21.     ~AudioTrackJniStorage() {  
  22.         mMemBase.clear();  
  23.         mMemHeap.clear();  
  24.     }  
  25.     /** 
  26.      * 分配一块指定大小的匿名共享内存 
  27.      * @param sizeInBytes:匿名共享内存大小 
  28.      * @return 
  29.      */  
  30.     bool allocSharedMem(int sizeInBytes) {  
  31.          //创建一个匿名共享内存  
  32.         mMemHeap = new MemoryHeapBase(sizeInBytes, 0, "AudioTrack Heap Base");  
  33.         if (mMemHeap->getHeapID() < 0) {  
  34.             return false;  
  35.         }  
  36.         mMemBase = new MemoryBase(mMemHeap, 0, sizeInBytes);  
  37.         return true;  
  38.     }  
  39. };  
  40.   
  41. /** 
  42.  * 创建匿名共享内存区域 
  43.  * @param size:匿名共享内存大小 
  44.  * @param flags:创建标志位 
  45.  * @param name:匿名共享内存名称 
  46.  */  
  47. MemoryHeapBase::MemoryHeapBase(size_t size, uint32_t flags, char const * name)  
  48. : mFD(-1), mSize(0), mBase(MAP_FAILED), mFlags(flags),  
  49.   mDevice(0), mNeedUnmap(false), mOffset(0)  
  50. {  
  51.     //获取内存页大小  
  52.     const size_t pagesize = getpagesize();  
  53.     //字节对齐  
  54.     size = ((size + pagesize-1) & ~(pagesize-1));  
  55.     /* 创建共享内存,打开/dev/ashmem设备,得到一个文件描述符 */  
  56.     int fd = ashmem_create_region(name == NULL ? "MemoryHeapBase" : name, size);  
  57.     ALOGE_IF(fd<0, "error creating ashmem region: %s", strerror(errno));  
  58.     if (fd >= 0) {  
  59.         //通过mmap将匿名共享内存映射到当前进程地址空间  
  60.         if (mapfd(fd, size) == NO_ERROR) {  
  61.             if (flags & READ_ONLY) {  
  62.                 ashmem_set_prot_region(fd, PROT_READ);  
  63.             }  
  64.         }  
  65.     }  
  66. }  

初始化AudioTrack

为AudioTrack设置音频参数信息,在Android4.4中,增加了一个参数transfer_type用于指定音频数据的传输方式,Android4.4定义了4种音频数据传输方式:

enum transfer_type {

            TRANSFER_DEFAULT,   // not specified explicitly; determine from the other parameters

            TRANSFER_CALLBACK,  // callback EVENT_MORE_DATA

            TRANSFER_OBTAIN,    // FIXME deprecated: call obtainBuffer() and releaseBuffer()

            TRANSFER_SYNC,      // synchronous write()

            TRANSFER_SHARED,    // shared memory

};

[cpp]  view plain  copy
  1. /** 
  2.  * 初始化AudioTrack 
  3.  * @param streamType  音频流类型 
  4.  * @param sampleRate  采样率 
  5.  * @param format      音频格式 
  6.  * @param channelMask 输出声道 
  7.  * @param frameCount  帧数 
  8.  * @param flags       输出标志位 
  9.  * @param cbf   Callback function. If not null, this function is called periodically 
  10.  *   to provide new data and inform of marker, position updates, etc. 
  11.  * @param user   Context for use by the callback receiver. 
  12.  * @param notificationFrames   The callback function is called each time notificationFrames          *  PCM frames have been consumed from track input buffer. 
  13.  * @param sharedBuffer 共享内存  
  14.  * @param threadCanCallJava  
  15.  * @param sessionId                  
  16.  * @return 
  17.  */  
  18. status_t AudioTrack::set(  
  19.         audio_stream_type_t streamType,  
  20.         uint32_t sampleRate,  
  21.         audio_format_t format,  
  22.         audio_channel_mask_t channelMask,  
  23.         int frameCountInt,  
  24.         audio_output_flags_t flags,  
  25.         callback_t cbf,  

猜你喜欢

转载自blog.csdn.net/longfei1986/article/details/94721716