zoukankan      html  css  js  c++  java
  • SoundPool 音频播放 API 详解【示例】

    一个大坑:SoundPool最多只能播放时长10s左右、大小70kb左右(这些值是我多次测试而来,并不确定)的音频!

    官方文档


    The SoundPool class manages and plays audio resources for applications.
    SoundPool类管理和播放应用程序的音频资源

    A SoundPool is a collection of samples that can be loaded into memory from a resource inside the APK or from a file in the file system. The SoundPool library uses the MediaPlayer service to decode the audio into a raw 16-bit PCM mono or stereo stream. This allows applications to ship with compressed streams without having to suffer the CPU load and latency of decompressing during playback.
    SoundPool是可以从APK内的资源或文件系统中的文件加载到内存中的样本集合。 SoundPool库使用MediaPlayer服务将音频解码为原始的16位PCM单声道或立体声流。 这允许应用程序使用压缩流进行传输,而不必在播放期间遭受CPU负载和解压缩的延迟。

    In addition to low-latency playback, SoundPool can also manage the number of audio streams being rendered at once. When the SoundPool object is constructed, the maxStreams parameter sets the maximum number of streams that can be played at a time from this single SoundPool. SoundPool tracks the number of active streams. If the maximum number of streams is exceeded, SoundPool will automatically stop a previously playing stream based first on priority and then by age within that priority. Limiting the maximum number of streams helps to cap CPU loading and reducing the likelihood that audio mixing will impact visuals or UI performance.
    除了低延迟播放之外,SoundPool还可以管理立即呈现的音频流的数量。当SoundPool对象被构造时,maxStreams参数设置从一个SoundPool可以一次播放的最大流数。 SoundPool跟踪活动流的数量。如果超过流的最大数量,SoundPool将首先优先按照该优先级的年龄自动停止先前播放的流。限制流的最大数量有助于加快CPU加载速度,并减少音频混合影响视觉效果或UI性能的可能性。

    Sounds can be looped by setting a non-zero loop value. A value of -1 causes the sound to loop forever. In this case, the application must explicitly call the stop() function to stop the sound. Any other non-zero value will cause the sound to repeat the specified number of times, e.g. a value of 3 causes the sound to play a total of 4 times.
    可以通过设置非零的值来循环播放声音。值为-1会使声音永久循环。在这种情况下,应用程序必须显式地调用stop()函数来停止声音。任何其他非零值将导致声音重复指定次数,例如一个值3会使声音共播放4次。

    The playback rate can also be changed. A playback rate of 1.0 causes the sound to play at its original frequency (resampled, if necessary, to the hardware output frequency). A playback rate of 2.0 causes the sound to play at twice its original frequency, and a playback rate of 0.5 causes it to play at half its original frequency. The playback rate range is 0.5 to 2.0.
    播放速度也可以改变。播放速率为1.0会导致声音以其原始频率播放(如有必要,重新采样到硬件输出频率)。 2.0的播放速度使声音以原始频率的两倍播放,并且播放速率为0.5使其以原始频率的一半播放。播放速度范围为0.5〜2.0。

    Priority runs low to high, i.e. higher numbers are higher priority. Priority is used when a call to play() would cause the number of active streams to exceed the value established by the maxStreams parameter when the SoundPool was created. In this case, the stream allocator will stop the lowest priority stream. If there are multiple streams with the same low priority, it will choose the oldest stream to stop. In the case where the priority of the new stream is lower than all the active streams, the new sound will not play and the play() function will return a streamID of zero.
    优先级从低到高,即较高的数字是较高的优先级。当play()的调用将导致,活动流的数量超过创建SoundPool时maxStreams参数建立的值时,将使用优先级。在这种情况下,流分配器将停止最低优先级流。如果存在多个具有相同低优先级的流,则它将选择最旧的流来停止。在新流的优先级低于所有活动流的情况下,新的声音将不会播放,并且play()函数将返回0的streamID。

    Let's examine a typical use case: A game consists of several levels of play. For each level, there is a set of unique sounds that are used only by that level. In this case, the game logic should create a new SoundPool object when the first level is loaded. The level data itself might contain the list of sounds to be used by this level. The loading logic iterates through the list of sounds calling the appropriate SoundPool.load() function. This should typically be done early in the process to allow time for decompressing the audio to raw PCM format before they are needed for playback.
    我们来研究一个典型的用例:游戏由几个级别组成。对于每个级别,都有一组仅由该级使用的唯一声音。在这种情况下,游戏逻辑应在第一级加载完成时创建一个新的SoundPool对象。级别数据本身可能包含此级别要使用的声音数据列表。加载逻辑迭代器通过声音列表调用相应的SoundPool.load()函数。这通常应该在进程的早期完成,以便在播放之前需要时间将音频解压缩为原始的PCM格式。

    Once the sounds are loaded and play has started, the application can trigger sounds by calling SoundPool.play(). Playing streams can be paused or resumed, and the application can also alter the pitch by adjusting the playback rate in real-time for doppler or synthesis effects.
    一旦声音被加载并且播放已经开始,应用程序可以通过调用SoundPool.play()来触发声音。播放流可以暂停或恢复,并且应用程序还可以通过实时调整多普勒或综合效果的播放速率来改变音调。

    Note that since streams can be stopped due to resource constraints, the streamID is a reference to a particular instance of a stream. If the stream is stopped to allow a higher priority stream to play, the stream is no longer be valid. However, the application is allowed to call methods on the streamID without error. This may help simplify program logic since the application need not concern itself with the stream lifecycle.
    注意,由于资源约束可以停止流,所以streamID是对流的特定实例的引用。如果流停止以允许更高优先级的流播放,流不再有效。但是,允许应用程序调用streamID上的方法,而不会出错。这可能有助于简化程序逻辑,因为应用程序不需要关注流生命周期。

    In our example, when the player has completed the level, the game logic should call SoundPool.release() to release all the native resources in use and then set the SoundPool reference to null. If the player starts another level, a new SoundPool is created, sounds are loaded, and play resumes.
    在我们的示例中,当玩家已经完成了级别时,游戏逻辑应该调用SoundPool.release()来释放所有正在使用的本地资源,然后将SoundPool引用设置为null。如果玩家开始另一个级别,则会创建一个新的SoundPool,声音被加载,播放恢复。

    API 介绍

    构造方法(废弃了)
    • SoundPool(int maxStreams, int streamType, int srcQuality)    This constructor was deprecated in API level 21. use SoundPool.Builder instead to create and configure a SoundPool instance
      • maxStream:同时播放的流的最大数量
      • streamType流的类型,一般为STREAM_MUSIC(具体在AudioManager类中列出)
      • srcQuality采样率转化质量,当前无效果,使用0作为默认值

    对指定streamID的播放进行控制

    • final void    pause(int streamID)    Pause a playback stream.  暂停播放
      • Pause the stream specified by the streamID. This is the value returned by the play() function. If the stream is playing, it will be paused. If the stream is not playing (e.g. is stopped or was previously paused), calling this function will have no effect. 暂停streamID指定的流。 这是play()函数返回的值。 如果流正在播放,它将被暂停。 如果流未播放(例如,停止或先前已暂停),则调用此功能将不起作用。
    • final void    resume(int streamID)    Resume a playback stream. 继续播放
      • Resume the stream specified by the streamID. This is the value returned by the play() function. If the stream is paused, this will resume playback. If the stream was not previously paused, calling this function will have no effect.
    • final void    stop(int streamID)    Stop a playback stream.  终止播放
      • Stop the stream specified by the streamID. This is the value returned by the play() function. If the stream is playing, it will be stopped. It also releases any native resources associated with this stream. If the stream is not playing, it will have no effect.

    对指定streamID的参数进行设置

    • final void    setLoop(int streamID, int loop)    Set loop mode.  设置指定播放流循环次数
      • A loop value of -1 means loop forever, a value of 0 means don't loop, other values indicate the number of repeats, e.g. a value of 1 plays the audio twice. If the stream does not exist, it will have no effect.
    • final void    setPriority(int streamID, int priority)    Change stream priority.  设置指定播放流的优先级
      • Change the priority of the stream specified by the streamID. This is the value returned by the play() function. Affects the order in which streams are re-used to play new sounds. If the stream does not exist, it will have no effect.
    • final void    setRate(int streamID, float rate)    Change playback rate.  设置指定播放流的播放速率
      • The playback rate allows the application to vary the playback rate (pitch) of the sound. A value of 1.0 means playback at the original frequency. A value of 2.0 means playback twice as fast, and a value of 0.5 means playback at half speed. If the stream does not exist, it will have no effect.
    • final void    setVolume(int streamID, float leftVolume, float rightVolume)    Set stream volume.  设置指定播放流的音量大小
      • Sets the volume on the stream specified by the streamID. This is the value returned by the play() function. The value must be in the range of 0.0 to 1.0. If the stream does not exist, it will have no effect.

    全部流的暂停播放与恢复播放

    • final void    autoPause()    Pause all active streams.
      • Pause all streams that are currently playing. This function iterates through all the active streams and pauses any that are playing. It also sets a flag so that any streams that are playing can be resumed by calling autoResume().  暂停正在播放的所有流。 此函数遍历所有活动流并暂停播放任何正在播放的流。 它还设置一个标志,以便可以通过调用autoResume()来恢复正在播放的任何流。
    • final void    autoResume()    Resume all previously active streams.
      • Automatically resumes all streams that were paused in previous calls to autoPause().

    资源释放

    • final boolean    unload(int soundID)    Unload a sound from a sound ID.  卸载一个指定的音频资源
      • Unloads the sound specified by the soundID. This is the value returned by the load() function. Returns true if the sound is successfully unloaded, false if the sound was already unloaded.
    • final void    release()    Release the SoundPool resources.  释放所有资源
      • Release all memory and native resources used by the SoundPool object. The SoundPool can no longer be used and the reference should be set to null. 

    设置监听

    • void    setOnLoadCompleteListener(SoundPool.OnLoadCompleteListener listener)    Sets the callback hook for the OnLoadCompleteListener.
    内部接口:SoundPool.OnLoadCompleteListener
    • void onLoadComplete(SoundPool soundPool, int sampleId, int status)  Called when a sound has completed loading.

    加载音频资源【load】

    可以通过以下四种途径来加载一个音频资源(API中指出,其中的priority参数目前没有效果,建议设置为1)
    • 通过一个资源ID,int load(Context context, int resId, int priority)
    • 通过指定的路径,int load(String path, int priority)
    • 通过一个AssetFileDescriptor对象,int load(AssetFileDescriptor afd, int priority)
    • 通过FileDescriptor,int load(FileDescriptor fd, long offset, long length, int priority)
    一个SoundPool能同时管理多个音频,所以可以通过多次调用load函数来加载,如果加载成功将返回一个非0的soundID,用于播放时指定特定的音频。 

    四种load方式
    • int  soundID  load(Context context, int resId, int priority)    Load the sound from the specified APK resource.
      • Note that the extension is dropped. For example, if you want to load a sound from the raw resource file "explosion.mp3", you would specify "R.raw.explosion" as the resource ID. Note that this means you cannot have both an "explosion.wav" and an "explosion.mp3" in the res/raw directory. 请注意,扩展名被删除。 例如,如果要从raw资源文件“explosion.mp3”加载声音,则应该指定“R.raw.explosion”作为资源ID。 请注意,这意味着您在res / raw目录中不能同时拥有“explosion.wav”和“explosion.mp3”文件。
      • int priority: the priority of the sound. Currently has no effect. Use a value of 1 for future compatibility.
      • Returnssound ID. This value can be used to play or unload the sound.
    • int  soundID  load(String path, int priority)    Load the sound from the specified path.
    • int  soundID  load(AssetFileDescriptor afd, int priority)    Load the sound from an asset file descriptor.
    • int  soundID  load(FileDescriptor fd, long offset, long length, int priority)    Load the sound from a FileDescriptor.
      • This version is useful if you store multiple sounds in a single binary. The offset specifies the offset from the start of the file and the length specifies the length of the sound within the file. 如果您将多个声音存储在单个二进制文件中,则此版本很有用。 offset指定从文件开头的偏移量,length指定文件中声音的长度。

    播放指定音频【play】

    播放指定的soundID,并返回一个streamID
    1. final int play(int soundID, float leftVolume, float rightVolume, int priority, int loop, float rate) 
    Play the sound specified by the soundID. This is the value returned by the load() function. Returns a non-zero streamID if successful, zero if it fails. The streamID can be used to further control playback. Note that calling play() may cause another sound to stop playing if the maximum number of active streams is exceeded
    播放soundID指定的声音。 这是load()函数返回的值。 如果成功,返回非零streamID;如果失败,返回0。 streamID可用于进一步控制播放。 请注意,如果超过最大数量的活动流,呼叫play()可能会导致另一个声音停止播放。

    A loop value of -1 means loop forever, a value of 0 means don't loop, other values indicate the number of repeats, e.g. a value of 1 plays the audio twice. The playback rate allows the application to vary the playback rate (pitch) of the sound. A value of 1.0 means play back at the original frequency. A value of 2.0 means play back twice as fast, and a value of 0.5 means playback at half speed.
    循环值为-1表示循环永远,值为0表示不循环,其他值表示重复次数,例如。 值为1播放音频两次。 播放速率允许应用程序改变声音的播放速率(音高)。 值为1.0表示以原始频率播放。 值为2.0意味着播放速度快两倍,值为0.5表示以半速播放。

    Parameters
    • soundID int: a soundID returned by the load() function
    • leftVolume float: left volume value (range = 0.0 to 1.0)  左声道的音量
    • rightVolume float: right volume value (range = 0.0 to 1.0)
    • priority int: stream priority (0 = lowest priority)  流的优先级
    • loop int: loop mode (0 = no loop, -1 = loop forever)
    • rate float: playback rate (1.0 = normal playback, range 0.5 to 2.0)  播放的速率
    Returns
    • int non-zero streamID if successful, zero if failed

    内部类:SoundPool.Builder

    • SoundPool.Builder()   Constructs a new Builder with the defaults format values.
      • If not provided, the maximum number of streams is 1 , and the audio attributes have a usage value of USAGE_MEDIA (see setAudioAttributes(AudioAttributes) to change them).
    • SoundPool.Builder   setAudioAttributes(AudioAttributes attributes)  Sets the AudioAttributes.
      • Sets the AudioAttributes. For examples, game applications will use attributes built with usage information set to USAGE_GAME.
      • AudioAttributes:A class to encapsulate a collection of attributes describing information about an audio stream. 封装描述有关音频流的信息的属性集合的类。
    • SoundPool.Builder   setMaxStreams(int maxStreams)  Sets the maximum of number of simultaneous streams that can be played simultaneously.
    • SoundPool   build()

    个人对SoundPool的认识与总结

    如果应用程序经常播放密集、急促而又短暂的音效(如游戏音效)那么使用MediaPlayer显得有些不太适合了。因为MediaPlayer存在如下缺点:
    • 1) 延时时间较长,且资源占用率高。
    • 2) 不支持多个音频同时播放。

    Android中除了MediaPlayer播放音频之外还提供了SoundPool来播放音效,SoundPool使用【音效池】的概念来管理多个短促的音效,例如它可以开始就加载20个音效,以后在程序中按音效的ID进行播放。

    SoundPool主要用于播放一些较短的声音片段,与MediaPlayer相比,SoundPool的优势在于CPU资源占用量低和反应延迟小

    SoundPool和其他声音播放类相比,其特点是可以自行设置声音的品质、音量、播放比率等参数。并且它可以同时管理多个音频流,每个流都有独自的ID,对某个音频流的管理都是通过ID进行的。

    实际使用SoundPool播放声音时需要注意:
    • SoundPool最大只能申请1M的内存空间,这就意味着我们只能用一些很短的声音片段,而不是用它来播放歌曲或者做游戏背景音乐。
    • SoundPool提供了pause和stop方法,但这些方法建议最好不要轻易使用,因为有些时候它们可能会使你的程序莫名其妙的终止。有些朋友反映它们不会立即中止播放声音,而是把缓冲区里的数据播放完才会停下来,也许会多播放一秒钟。
    • 流的加载过程是一个将音频解压为原始16位PCM数据的过程,由一个后台线程来进行异步处理,所以初始化后不能立即播放,需要等待一点时间。
    • SoundPool的效率在这些播放类中算是很好的了,但也不是绝对不存在延迟问题,尤其在那些性能不太好的手机中,SoundPool的延迟问题可能会很严重。

    总结:
    • 1、一个SoundPool可以管理多个音频资源,通过load()函数,成功则返回非0的soundID
    • 2、一个SoundPool可以同时播放多个音频资源,通过play()函数,成功则返回非0的streamID
    • 3、pause()、resume()和stop()等操作是针对streamID(播放流)的
    • 4、当设置为无限循环时,需要手动调用stop()来终止播放
    • 5、播放流的优先级(play()中的priority参数),只在同时播放数超过设定的最大数时起作用
    • 6、程序中不用考虑播放流的生命周期,无效的soundID/streamID不会导致程序错误

    演示示例

    1. public class SoundPoolActivity extends ListActivity {
    2. private SoundPool soundPool;
    3. private List<Integer> soundIdList = new ArrayList<>();
    4. protected void onCreate(Bundle savedInstanceState) {
    5. super.onCreate(savedInstanceState);
    6. String[] array = {
    7. //0-6
    8. "通过一个资源ID:R.raw.s1_message,无限循环",
    9. "通过一个资源ID:R.raw.s10_42kb,能完整播放,循环2次,速度1.5倍,音量右0.1",
    10. "通过一个资源ID:caravan_15s_59kb,能完整播放,速度0.5倍,音量左0.1",
    11. "通过一个资源ID:R.raw.s8_67kb,虽然时间短,占空间也小,但是不能完全播放",
    12. "通过一个资源ID:ljsw_35s_68kb,虽然时间比较长,但是能完整播放",
    13. "通过一个资源ID:ljsw_49s_102kb,但是这个就不能完整播放了",
    14. "通过一个资源ID:hellow_tomorrow_6s_237kb,虽然占空间比较大,但是也能完整播放",
    15. //7-10
    16. "通过指定的路径:文件路径,caravan.mp3(不能播放网络资源)",
    17. "通过AssetFileDescriptor:assets目录下的文件,caravan_15s_59kb.mp3",
    18. "通过FileDescriptor:assets目录下的文件,可以播放文件指定的某一部分",
    19. "通过AssetFileDescriptor:raw目录下的文件,R.raw.s1_system",
    20. //11-14
    21. "全部流的暂停播放",
    22. "恢复播放",
    23. "卸载指定soundId的音频资源",
    24. "释放全部资源"
    25. };
    26. for (int i = 0; i < array.length; i++) {
    27. array[i] = i + "、" + array[i];
    28. }
    29. setListAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, new ArrayList<>(Arrays.asList(array))));
    30. soundPool = new SoundPool.Builder()
    31. .setMaxStreams(5)
    32. .setAudioAttributes(new AudioAttributes.Builder()
    33. .setUsage(AudioAttributes.USAGE_MEDIA)
    34. .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC).build())
    35. .build();
    36. soundPool.setOnLoadCompleteListener((soundPool1, sampleId, status) -> {
    37. Log.i("bqt", "【onLoadComplete】" + sampleId + " " + status);//status : 0 = success
    38. });
    39. new Thread(this::initReseResources).start();//在子线程中初始化,特别是当有大量资源需要初始化时
    40. }
    41. private void initReseResources() {
    42. Context context = this.getApplicationContext();//不管使用哪个Context,退出当前Activity后都会且会在延迟几秒钟后停止播放
    43. //0-6,通过一个资源ID:Context context, int resId, int priority
    44. soundIdList.add(soundPool.load(context, R.raw.s1_message, 0));
    45. soundIdList.add(soundPool.load(context, R.raw.s10_42kb, 0));
    46. soundIdList.add(soundPool.load(context, R.raw.caravan_15s_59kb, 0));
    47. soundIdList.add(soundPool.load(context, R.raw.s8_67kb, 0));
    48. soundIdList.add(soundPool.load(context, R.raw.ljsw_35s_68kb, 0));
    49. soundIdList.add(soundPool.load(context, R.raw.ljsw_49s_102kb, 0));
    50. soundIdList.add(soundPool.load(context, R.raw.hellow_tomorrow_6s_237kb, 0));
    51. //7,通过指定的路径:String path, int priority
    52. soundIdList.add(soundIdList.size(), 0);//如果文件不存在,则就不能set,否则会throw IndexOutOfBoundsException
    53. String path = Environment.getExternalStorageDirectory() + File.separator + "caravan.mp3";
    54. soundIdList.set(soundIdList.size() - 1, soundPool.load(path, 0));//注意:add和set时传入的index是不一样的!
    55. //8-9,通过AssetFileDescriptor:AssetFileDescriptor afd, int priority
    56. try {
    57. soundIdList.add(soundIdList.size(), 0);//为防止异常后后续的代码执行不到导致index错乱,我们在一开始就直接add两个
    58. soundIdList.add(soundIdList.size(), 0);
    59. AssetFileDescriptor afd = getAssets().openFd("voice/caravan_15s_59kb.mp3");//openNonAssetFd
    60. soundIdList.set(soundIdList.size() - 2, soundPool.load(afd, 0));
    61. //通过FileDescriptor:FileDescriptor fd, long offset, long length, int priority
    62. FileDescriptor fd = afd.getFileDescriptor();
    63. long offset = afd.getStartOffset(), length = afd.getLength();
    64. Log.i("bqt", "【afd】offset=" + offset + ",length=" + length);//offset=40786180,length=60480
    65. soundIdList.set(soundIdList.size() - 1, soundPool.load(fd, offset + length / 2, length / 2, 0));
    66. } catch (IOException e) {
    67. e.printStackTrace();
    68. }
    69. //10
    70. soundIdList.add(soundPool.load(getResources().openRawResourceFd(R.raw.s1_global), 0));
    71. }
    72. @Override
    73. protected void onDestroy() {
    74. super.onDestroy();
    75. soundPool.release();//释放所有资源
    76. }
    77. @Override
    78. protected void onListItemClick(ListView l, View v, int position, long id) {
    79. if (position <= 10) {
    80. int soundID = soundIdList.get(position);
    81. int loop = 0;//0 = no loop, -1 = loop forever
    82. if (position == 0) loop = -1;
    83. else if (position == 1) loop = 2;//3次
    84. //播放指定soundID的音频:【int soundID】, float leftVolume, float rightVolume,int priority, 【int loop】, float rate
    85. int streamID = soundPool.play(soundID, 1.0f, 1.0f, 1, loop, 1.0f);
    86. Toast.makeText(this, "soundID=" + soundID + " streamID=" + streamID, Toast.LENGTH_SHORT).show();
    87. //指定streamID的参数进行设置,这些参数都可以在播放时指定
    88. soundPool.setLoop(streamID, 1);//实践证明,这里设置无效
    89. switch (position) {
    90. case 1:
    91. soundPool.setRate(streamID, 1.5f);//大小并没有限制,但是一般不要小于0.5,不要大于1.5,否则声音严重失真
    92. soundPool.setVolume(streamID, 1.0f, 0.1f);//只能降低,不能提高。The value must be in the range of 0.0 to 1.0
    93. break;
    94. case 2:
    95. soundPool.setRate(streamID, 0.5f);
    96. soundPool.setVolume(streamID, 0.1f, 1.0f);
    97. break;
    98. }
    99. } else {
    100. switch (position) {
    101. case 11:
    102. soundPool.autoPause();//可以多次调用,每次都是把当前正在播放的音乐暂停,并加入同一个列表中
    103. break;
    104. case 12:
    105. soundPool.autoResume();//将所有暂停的音乐从暂停位置重新开始播放
    106. break;
    107. case 13:
    108. Toast.makeText(this, "" + soundPool.unload(soundIdList.get(0)) + " " + soundPool.unload(soundIdList.get(1))
    109. , Toast.LENGTH_SHORT).show();//unload并不能停止正在播放的音乐,特别是loop=-1的,仍会循环播放
    110. break;
    111. case 14:
    112. soundPool.release();//会停止正在播放的所有音乐
    113. break;
    114. }
    115. }
    116. }
    117. }
    2017-7-5




  • 相关阅读:
    Oracle SQL 函数
    j2me MIDP2.0 下实现split函数
    Linux Oracle 增量恢复时错误 ORA19573: 无法获得 exclusive 入队 (数据文件 5 的)
    Linux Oracle10 建立归档模式的详细过程
    j2me MIDP2.0 下实现的图片缩放函数
    linux下oracle10g建立归档模式 接连出现错误:ORA19905 ORA01078 LRM00109
    j2me下 触摸屏的开发 NetBeans 模拟器支持触摸屏
    高级程序员:你不可不知的20条编程经验(转载)
    生成规定大小的图片(缩略图生成)
    asp.net简单实现用button做按钮图片
  • 原文地址:https://www.cnblogs.com/baiqiantao/p/7145305.html
Copyright © 2011-2022 走看看