HardBirch

Android提高第一篇之MediaPlayer

时间:10-10-30 栏目:安卓入门与提高 作者:张飞不张,文采横飞 评论:85 点击: 39,851 次

        前面写了十四篇关于界面的入门文章,大家都看完和跟着练习之后,对于常用的Layout和View都会有一定的了解了,接下来的文章就不再强调介绍界面了,而是针对具体的常见功能而展开。

        本文介绍MediaPlayer的使用。MediaPlayer可以播放音频和视频,另外也可以通过VideoView来播放视频,虽然VideoView比MediaPlayer简单易用,但定制性不如用MediaPlayer,要视情况选择了。MediaPlayer播放音频比较简单,但是要播放视频就需要SurfaceView。SurfaceView比普通的自定义View更有绘图上的优势,它支持完全的OpenGL ES库。

         先贴出本文程序运行结果的截图,上面是播放/停止音频,可用SeekBar来调进度,下面是播放/停止视频,也是用SeekBar来调进度:

main.xml的源码:
















<SurfaceView android:id="@+id/SurfaceView01"
android:layout_width="fill_parent" android:layout_height="250px"></SurfaceView>
<LinearLayout android:id="@+id/LinearLayout02"
android:layout_width="wrap_content" android:layout_height="wrap_content">
<Button android:layout_width="wrap_content"
android:layout_height="wrap_content" android:id="@+id/Button03"
android:text="播放视频"></Button>
<Button android:layout_width="wrap_content"
android:layout_height="wrap_content" android:text="停止播放" android:id="@+id/Button04"></Button>
</LinearLayout>
</LinearLayout>

本文程序的源码,有点长:

import java.io.IOException;
import java.util.Timer;
import java.util.TimerTask;
import android.app.Activity;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.widget.Button;
import android.widget.SeekBar;
import android.widget.Toast;

public class testMedia extends Activity {
/** Called when the activity is first created. */

private SeekBar skb_audio=null;
private Button btn_start_audio = null;
private Button btn_stop_audio = null;

private SeekBar skb_video=null;
private Button btn_start_video = null;
private Button btn_stop_video = null;
private SurfaceView surfaceView;
private SurfaceHolder surfaceHolder;

private MediaPlayer m = null;
private Timer mTimer;
private TimerTask mTimerTask;

private boolean isChanging=false;//互斥变量,防止定时器与SeekBar拖动时进度冲突
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

//----------Media控件设置---------//
m=new MediaPlayer();

//播放结束之后弹出提示
m.setOnCompletionListener(new MediaPlayer.OnCompletionListener(){
@Override
public void onCompletion(MediaPlayer arg0) {
Toast.makeText(testMedia.this, "结束", 1000).show();
m.release();
}
});

//----------定时器记录播放进度---------//
mTimer = new Timer();
mTimerTask = new TimerTask() {
@Override
public void run() {
if(isChanging==true)
return;

if(m.getVideoHeight()==0)
skb_audio.setProgress(m.getCurrentPosition());
else
skb_video.setProgress(m.getCurrentPosition());
}
};

mTimer.schedule(mTimerTask, 0, 10);

btn_start_audio = (Button) this.findViewById(R.id.Button01);
btn_stop_audio = (Button) this.findViewById(R.id.Button02);
btn_start_audio.setOnClickListener(new ClickEvent());
btn_stop_audio.setOnClickListener(new ClickEvent());
skb_audio=(SeekBar)this.findViewById(R.id.SeekBar01);
skb_audio.setOnSeekBarChangeListener(new SeekBarChangeEvent());

btn_start_video = (Button) this.findViewById(R.id.Button03);
btn_stop_video = (Button) this.findViewById(R.id.Button04);
btn_start_video.setOnClickListener(new ClickEvent());
btn_stop_video.setOnClickListener(new ClickEvent());
skb_video=(SeekBar)this.findViewById(R.id.SeekBar02);
skb_video.setOnSeekBarChangeListener(new SeekBarChangeEvent());
surfaceView = (SurfaceView) findViewById(R.id.SurfaceView01);
surfaceHolder = surfaceView.getHolder();
surfaceHolder.setFixedSize(100, 100);
surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}

/*
* 按键事件处理
*/
class ClickEvent implements View.OnClickListener{
@Override
public void onClick(View v) {
if(v==btn_start_audio)
{
m.reset();//恢复到未初始化的状态
m=MediaPlayer.create(testMedia.this, R.raw.big);//读取音频
skb_audio.setMax(m.getDuration());//设置SeekBar的长度
try {
m.prepare(); //准备
} catch (IllegalStateException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
m.start(); //播放
}
else if(v==btn_stop_audio || v==btn_stop_video)
{
m.stop();
}
else if(v==btn_start_video)
{
m.reset();//恢复到未初始化的状态
m=MediaPlayer.create(testMedia.this, R.raw.test);//读取视频
skb_video.setMax(m.getDuration());//设置SeekBar的长度
m.setAudioStreamType(AudioManager.STREAM_MUSIC);
m.setDisplay(surfaceHolder);//设置屏幕

try {
m.prepare();

} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalStateException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
m.start();
}
}
}

/*
* SeekBar进度改变事件
*/
class SeekBarChangeEvent implements SeekBar.OnSeekBarChangeListener{

@Override
public void onProgressChanged(SeekBar seekBar, int progress,
boolean fromUser) {
// TODO Auto-generated method stub

}

@Override
public void onStartTrackingTouch(SeekBar seekBar) {
isChanging=true;
}

@Override
public void onStopTrackingTouch(SeekBar seekBar) {
m.seekTo(seekBar.getProgress());
isChanging=false;
}

}

}

声明: 本文由( 张飞不张,文采横飞 )原创编译,转载请保留链接: Android提高第一篇之MediaPlayer

Android提高第一篇之MediaPlayer:目前有85 条留言

  1. 0楼
    kf156:

    [e01] 越来越深入了啊,跟紧虫哥脚步!

    2010-10-30 11:41 [回复]
  2. 群成员前来顶贴

    2010-10-30 11:48 [回复]
  3. 0楼
    kk508:

    [e01]路过学下下

    2010-11-01 00:28 [回复]
  4. 0楼
    hmc1985:

    不错!学习一下![e01]

    2010-11-01 16:25 [回复]
  5. 前来观摩啊

    2010-11-02 20:48 [回复]
  6. 0楼
    lvhaiyan:

    学习,学习

    2010-11-03 10:02 [回复]
  7. 啥时候讨论讨论后台运行的视频播放器?

    2010-11-03 17:55 [回复]
  8. 一直保持学习![e01]

    2010-11-06 15:03 [回复]
  9. 0楼
    luiwei99:

    牛人,学习学习

    2010-11-09 14:52 [回复]
  10. 只能播放3gp呀

    2010-12-01 20:08 [回复]
  11. 0楼
    yangfan227:

    isChanging在两个线程里被访问,不用线程同步吗?

    2010-12-12 20:44 [回复]
  12. 0楼
    cindoralla:

    播放不了视频。。

    2010-12-20 16:57 [回复]
  13. 0楼
    xubing728:

    如果正常状态,prepare根本不会异常;如果状态不正常,create就报异常了,根本不会到prepare。因此prepare加异常捕获根本是多余代码,希望大家不要互相乱抄,很多书上也这么写,绝对是错误的。

    文档中已经说得很明白了:
    Furthermore, the MediaPlayer objects created using new is in the Idle state, while those created with one of the overloaded convenient create methods are NOT in the Idle state. In fact, the objects are in the Prepared state if the creation using create method is successful.

    因此create之后就不要多此一举的调用prepare()了!

    –android 源码开发者

    2010-12-31 10:35 [回复]
  14. 0楼
    kitis:

    m=MediaPlayer.create(testMedia.this, R.raw.test);//读取视频

    R.raw.test 在哪啊 汗 真的没找到 怎么把 视频文件放进去啊

    2011-01-06 15:42 [回复]
  15. 0楼
    kitis:

    回复 kitis: 我自己犯傻 已经解决了 汗

    2011-01-06 16:22 [回复]
  16. 0楼
    kitis:

    楼主 ,请问下 为什么 我点播放 只有声音没有图像呢?

    2011-01-06 16:23 [回复]
  17. 0楼
    chensylsl:

    问一下 为什么我老是系统崩溃呢?

    2011-01-13 09:30 [回复]
  18. 0楼
    xjmt42:

    播放视频的时候怎么有声音没画面的

    2011-01-14 10:50 [回复]
  19. 0楼
    gundumw100:

    你的这些教程非常棒,获益匪浅,谢谢。磕头…呵呵!

    2011-02-13 18:25 [回复]
  20. 为什么只有3gp格式的视频才能播放出画面而其他的格式只有声音没画面呢?

    2011-03-10 14:02 [回复]
  21. 0楼
    session88:

    [e07]getVideoHeight failed是什么原因啊?

    2011-03-14 17:03 [回复]
  22. 0楼
    session88:

    请问作者:getVideoHeight failed是什么原因啊?

    2011-03-14 17:03 [回复]
  23. 0楼
    session88:

    回复 ppooiiuuyy1122334455:[e07]getVideoHeight failed是什么原因啊?你有能抛弃来的源码吗?发给我 _775066@126.com 感激不尽

    2011-03-14 17:15 [回复]
  24. 0楼
    session88:

    回复 xubing728:[e07]getVideoHeight failed是什么原因啊?你有能抛弃来的源码吗?发给我 _775066@126.com 感激不尽

    2011-03-14 17:16 [回复]
  25. 0楼
    session88:

    回复 cindoralla:[e07]getVideoHeight failed是什么原因啊?你有能抛弃来的源码吗?发给我 _775066@126.com 感激不尽

    2011-03-14 17:17 [回复]
  26. 0楼
    session88:

    回复 kitis:[e07]getVideoHeight failed是什么原因啊?你有能抛弃来的源码吗?发给我 _775066@126.com 感激不尽

    2011-03-14 17:17 [回复]
  27. 0楼
    session88:

    回复 session88:是 Lang _775066@126.com

    2011-03-14 17:18 [回复]
  28. 0楼
    session88:

    回复 session88:是 Lang _775066@126.com

    2011-03-14 17:19 [回复]
  29. 0楼
    session88:

    回复 session88:是 Lang _775066@126.com

    2011-03-14 17:19 [回复]
  30. 0楼
    session88:

    回复 session88:是 Lang _775066@126.com

    2011-03-14 17:19 [回复]
  31. 0楼
    session88:

    [e07]hellogv,请问直接复制你的代码跑步起来,getVideoHeight failed 怎么回事啊?能不能把源码发布出来

    2011-03-15 15:13 [回复]
  32. 0楼
    chrisghr:

    [e01][e01][e01]牛

    2011-03-17 19:25 [回复]
  33. 上面的代码是有问题的,这个代码是播放本地应用程序下的视频文件,如果不明白的可以联系我写的帖子,链接http://www.cmd100.com/bbs/forum.php?mod=viewthread&amp;tid=8454&amp;extra=

    2011-03-25 14:39 [回复]
  34. 0楼
    hellogv:

    回复 ppooiiuuyy1122334455:
    是否在真机上?

    2011-03-25 15:34 [回复]
  35. 0楼
    yxy1008617:

    好东西。。正要用这个东西

    2011-04-14 15:24 [回复]
  36. 0楼
    korry520:

    运行上面代码,logcat中刷屏显示“getVideoHeight failed”错误。在源文件的定时器代码中,根据android_media_MediaPlayer.cpp这个JNI文件,只要m.getVideoHeight()!=0就出现该错误。

    2011-04-15 16:16 [回复]
  37. 0楼
    korry520:

    回复 korry520:注释掉if(m.getVideoHeight()==0)…else…,出现解码错误。总之,整个过程只有声音,没有画面。
    备注:播放本地应用程序下的3gp视频文件。

    曾经遇到该问题的朋友但后来又解决了的,麻烦给我发个消息,谢谢!

    2011-04-15 16:24 [回复]
  38. 0楼
    korry520:

    回复 korry520:真机和模拟器我都试过了,均只有声音,没有画面

    2011-04-15 16:27 [回复]
  39. 0楼
    hellogv:

    回复 korry520:
    迟点,我会重写这个例子的

    2011-04-15 17:22 [回复]
  40. [e01]学习了。

    2011-04-16 00:03 [回复]
  41. 0楼
    korry520:

    回复 hellogv:谢谢![e03]

    2011-04-18 09:06 [回复]
  42. 0楼
    korry520:

    回复 korry520:问题已解决!问题出现在视频文件的编码方式上。该程序支持播放MP4和3gp格式文件,但由于MP4和3gp有很多子格式,音频编码方式不同,所以导致有些音频文件播放只有声音没有画面

    2011-04-20 17:38 [回复]
  43. 0楼
    yqw1122:

    我播放3GP格式的视频出现只有声音没有图像的问题。[e08]

    2011-04-28 17:00 [回复]
  44. 我播放.mp4文件,有声音也有图像,但是声音和图像不能同步;进度条拖到哪图像就卡在哪。这是什么原因啊?
    小弟是还在门外徘徊的菜鸟,各位大虾多多指教!1186291613@qq.com

    2011-05-04 17:49 [回复]
  45. 有bug
    点击播放音频,等待自动锁屏
    再打开该程序,点击播放音频
    出现两个声音。

    2011-05-08 15:29 [回复]
  46. 0楼
    lvtao77:

    [e07]楼主 你这个canvas对象是null…surfacview没能创建

    2011-05-20 13:02 [回复]
  47. 问个问题啊。。。在程序中怎么获得已经在播放的mediaplay对象来关闭它呢

    2011-05-23 18:49 [回复]
  48. 回复 xubing728:正确,使用MediaPlayer.create方法,不再需要使用prepare();这个是多此一举!

    2011-05-25 16:34 [回复]
  49. 0楼
    hellogv:

    回复 vampire_333:
    全局保存mediaplay,关闭之前要判断是否为null

    2011-05-25 17:26 [回复]
  50. 0楼
    wjdarwin:

    回复 session88: 问题解决了么 。。 能告诉下么? 小白 求教

    2011-06-08 11:01 [回复]
  51. 回复 xubing728:确实如此!!!

    2011-06-21 17:19 [回复]
  52. 回复 xubing728:确实如此!!!

    2011-06-21 17:29 [回复]
  53. 0楼
    zzzkkk666:

    我也来学习

    2011-08-05 11:46 [回复]
  54. 0楼
    glason:

    [reply]korry520[/reply]
    应该把下面这些代码加入到View.OnClickListenner中
    mTimer = new Timer();
    mTimerTask = new TimerTask() {
    @Override
    public void run() {
    if(isChanging==true)
    return;

    if(m.getVideoHeight()==0)
    skb_audio.setProgress(m.getCurrentPosition());
    else
    skb_video.setProgress(m.getCurrentPosition());
    }
    };

    mTimer.schedule(mTimerTask, 0, 10);

    2011-08-19 19:53 [回复]
  55. 0楼
    glason:

    [reply]glason[/reply]
    因为在程序初始化的时候,源代码中把MediaPlayer.create()方法放在了OnClickListener中了,所以导致执行m.getVideoHeight()失败(此时还没有设置媒体源),so ,应该放在onClickListener中

    2011-08-19 19:56 [回复]
  56. [reply]yangjing3092[/reply]
    怎么帖子没在了,正想学习下

    2011-08-21 15:07 [回复]
  57. [reply]kitis[/reply]
    这个怎么放进去啊 我也不知道 请教
    我…菜鸟!

    2011-08-21 15:13 [回复]
  58. 请教大牛流媒体肿么做啊?

    2011-10-18 15:49 [回复]
  59. 写的真棒!!!

    2011-10-27 16:54 [回复]
  60. 0楼
    r8HZGEmq:

    哇塞,从没见过这么火爆的讨论。LZ很拉风,学习中

    2011-11-06 00:25 [回复]
  61. 0楼
    miklejhon:

    怎么解决这个问题啊

    2011-11-07 23:55 [回复]
  62. 0楼
    miklejhon:

    请问为什么我运行的时候出现以下错误
    [2011-11-07 23:50:41 - Android Project] Failed to install Android Project.apk on device 'emulator-5554': No space left on device
    [2011-11-07 23:50:41 - Android Project] com.android.ddmlib.SyncException: No space left on device
    [2011-11-07 23:50:41 - Android Project] Launch canceled!

    2011-11-07 23:55 [回复]
  63. 0楼
    hellogv:

    [reply]miklejhon[/reply]
    把模拟器删掉再新建一个试试

    2011-11-08 12:30 [回复]
  64. [reply]miklejhon[/reply]
    提示信息不是说了吗,是模拟器上没有空间了,因为你很长时间都是用一个模拟器测试很多程序,将这些程序安装进去之后没有清除过,不用删除模拟器这样太麻烦了。你打开DDMS,然后进入File Explorer,将你以前测试用的一些apk删除掉就行了

    2011-11-27 16:09 [回复]
  65. [reply]xubing728[/reply]
    说的很对,create如果返回成功,prepare已经在里面被调用过了而且也返回了成功,不用再去调用prepare

    2011-11-27 16:10 [回复]
  66. [reply]miklejhon[/reply]
    提示信息不是说了吗,是模拟器上没有空间了,因为你很长时间都是用一个模拟器测试很多程序,将这些程序安装进去之后没有清除过,不用删除模拟器这样太麻烦了。你打开DDMS,然后进入File Explorer,将你以前测试用的一些apk删除掉就行了

    2011-11-27 16:12 [回复]
  67. 0楼
    zhiyi2010:

    也能够播放swf文件吗

    2011-12-12 13:35 [回复]
  68. 0楼
    hellogv:

    [reply]zhiyi2010[/reply]
    我调用的是系统mediaplayer的api,这个看系统是否支持了

    2011-12-15 22:30 [回复]
  69. 0楼
    ameyume:

    [reply]zhiyi2010[/reply]
    系统的MediaPlayer默认是不支持swf格式文件的。

    2011-12-30 10:33 [回复]
  70. 只有声音,没有画面

    2012-01-05 10:24 [回复]
  71. 0楼
    tronteng:

    这位仁兄首先我很佩服你了解的范围很广啊!我是个初学者最近遇到个关于android4.0.3硬件解码的部分。我发现解码高清的时候会出现如下错误:
    不知道对于这个问题有什么建议?

    2012-02-20 09:38 [回复]
  72. 0楼
    tronteng:

    I/OMXCodec( 97): [OMX.TI.DUCATI1.VIDEO.DECODER] Crop rect is 1280 x 720 @ (0,
    0)
    I/OMXCodec( 97): [OMX.TI.DUCATI1.VIDEO.DECODER] video dimensions are 1408 x 75
    2
    I/OMXCodec( 97): [OMX.TI.DUCATI1.VIDEO.DECODER] Crop rect is 1280 x 720 @ (16,
    16)
    E/OMXCodec( 97): [OMX.google.aac.decoder] ERROR(0×80001001, 10)
    I/AudioPlayer( 97): mLatencyUs = 176000
    I/AudioPlayer( 97): TimeInterpolator state STOPPED -> FIRST_ROLLING (input: PO
    ST_BUFFER)
    I/AudioPlayer( 97): TimeInterpolator::seek(media_time=0)
    E/MediaPlayer( 589): error (1, -2147483648)
    I/AudioPlayer( 97): void omap_enhancement::TimeInterpolator::pause(bool)()

    2012-02-20 09:39 [回复]
  73. 0楼
    tronteng:

    I/OMXCodec( 97): [OMX.TI.DUCATI1.VIDEO.DECODER] video[ 266.405517] omap_hwmod
    : iva_seq0: failed to hardreset
    dimensions are [ 266.427001] omap_hwmod: iva_seq1: failed to hardreset
    1280 x 720
    I/OMXCodec( 97): [OMX.TI.DUCATI1.VIDEO.DECODER] Crop rect is 1280 x 720 @ (0,
    0)
    W/OMXCodec( 97): Number of channels: (6)
    D/MediaPlayer( 589): getMetadata
    I/AudioPlayer( 97): TimeInterpolator::seek(media_time=0)
    I/OMXCodec( 97): [OMX.TI.DUCATI1.VIDEO.DECODER] video dimensions are 1408 x 75
    2

    2012-02-20 09:39 [回复]
  74. 0楼
    hellogv:

    [reply]tronteng[/reply]
    没遇见过这种问题呢。。。你用手机播放高清?

    2012-02-20 22:58 [回复]
  75. 0楼
    tronteng:

    android4.0 google自带的那个Garelly图片浏览器,然后调用自带的播放器播放。

    2012-02-27 14:14 [回复]
  76. 0楼
    nesta102:

    楼主 请问一下播放不流畅有可能是什么原因?

    2012-03-16 09:35 [回复]
  77. 0楼
    davient:

    大家好,我根据这个代码进行了一点修改,增加了一个activity来选择视频文件,但是运行的时候出现异常,请大家帮忙看一看,增加代码如下:
    class ClickEvent implements View.OnClickListener {
    public void onClick(View v) {
    …} else if (v == btn_playvedio) {
    if (m.isPlaying()) m.stop();
    //用来选择文件
    String AudioDir = nvironment.getExternalStorageDirectory() .getAbsolutePath();
    Intent i = new Intent(MycameraActivity.this, ListFiles.class); i.putExtra("directory", AudioDir);
    startActivityForResult(i, 1);
    }
    //回调进行处理


    super.onActivityResult(requestCode, resultCode, data);
    if ((requestCode == 0 || requestCode == 1) && resultCode == RESULT_OK) {
    str_file = data.getExtras().getString("clickedFile");
    type = requestCode;
    if (type == 1) {
    try {

    surfaceview = (SurfaceView) m.setDisplay(surfaceview.getHolder());
    m.start();

    其中,运行到setdisplayer(surface.getHolder())时异常,logcat提示 the surface has been release,
    请问原因是什么

    2012-03-16 23:03 [回复]
  78. [reply]hellogv[/rep[reply]hellogv[/reply]

    我新建了一个还是不行

    2012-03-21 22:54 [回复]
  79. 0楼
    hellogv:

    [reply]nesta102[/reply]
    网络问题

    2012-03-22 09:09 [回复]
  80. 0楼
    happysshao:

    2012-04-17 21:23:48 – exp0417] ActivityManager: Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.sun.exp0417/.Exp0417Activity }
    [2012-04-17 21:23:48 - exp0417] ActivityManager: Warning: Activity not started, its current task has been brought to the front

    我把代码没有变动的写上,只是我人为的res的raw添加了一首big.mp3文件,怎么会出现上述错误呢?
    图形化界面出现了,但是就是没有反应。

    2012-04-17 21:41 [回复]
  81. 0楼
    hellogv:

    [reply]happysshao[/reply]
    加MP3之前的程序调通没

    2012-04-19 23:39 [回复]
  82. 0楼
    q694119254:

    相隔两年 继续顶

    2012-04-26 17:14 [回复]
  83. 0楼
    muriqui:

    博主,我对两行代码不解,求解惑。
    btn_start_video.setOnClickListener(new ClickEvent());
    …..
    class ClickEvent implements View.OnClickListener{ …..
    这两行的ClickEvent类都报了错误,分别是
    1.ClickEvent cannot be resolved to a type
    2.The type ClickListener must implement the inherited abstract method DialogInterface.OnClickListener.onClick(DialogInterface, int)
    到底是什么问题呢?ps:我已经有import android.view.View;了。

    2012-05-27 17:01 [回复]
  84. 0楼
    xu20101010:

    你好! 我有个问题想问一下 我现在有一个mp3文件 保存在一个byte[]中 怎么样 不通过创建中间文件的方式用mediaPlayer播放出来

    2012-07-24 22:54 [回复]
  85. cindoralla:[e07]getVideoHeight failed是什么原因啊?你有能抛弃来的源码吗?发给我zhengsiyuan1990@126.com 感激不尽

    2012-09-11 10:58 [回复]

发表评论


QQ群互动

Linux系统与内核学习群:194051772

WP建站技术学习交流群:194062106

魔豆之路QR

魔豆的Linux内核之路

魔豆的Linux内核之路

优秀工程师当看优秀书籍

优秀程序员,要看优秀书!

赞助商广告

友荐云推荐