Waveform Audio 驱动(Wavedev2)之:WAV 驱动解析

上篇文章中,我们模拟了WAV API。现在进入我们正在要解析的Wave 驱动的架构。我们了解一个驱动的时候,先不去看具体跟硬件操作相关的东西,而是从流程入手,把整个流程搞清楚了,调试起来就非常的容易了。我们着重看 hwctxt.cpp,hwctxt.H,devctxt.cpp,devctxt.H,strmctxt.cpp,strmctxt.H这几个源文件。 其中hwctxt是类HardwareContext代码文件,devctxt是DeviceContext代码文件,strmctxt是 StreamContext代码文件。这几个类的其他一些功能,还在其他一些文件中实现,如output.Cpp,midistrm.Cpp等。

现在我们来看下StreamContext的类图,StreamContext是管理音频流的对象,包括播放、暂停、停止、设置音量、获取播放位置等。从 下面的StreamContext的类图中,我们可以看到它派生了WaveStreamContext和MidiStream。然后 WaveStreamContext又派生了Input和Output类型的Stream。不用说也可以知道InputStreamContext是针对 于像麦克这种输入设备流的。

StreamContext类图

其中OutputStreamContext派生了六个类,M代表单音道,S代表的是立体音,8/16是8/16比特采样了。 SPDIF(SONY/PHILIPS DIGITAL INTERFACE)是一种最新的音频传输格式,它通过光纤进行数字音频信号传输以取代传统的模拟信号传输方式,因此可以取得更高质量的音质效果。

StreamContext是一个管理音频数据流的对象,像智能手机中可能存在用media player播放音乐,同时又开着FM,突然又来电。从上篇文章中我们知道,要想调用wave驱动的播放功能,每个应用都有一份 StreamContext对象,上面提到的状况,就会有三个StreamContext对象被创建。 在硬件只要一个的条件下,那么这三个StreamContext是如果协同工作的呢?而DeviceContext正是管理StreamContext对 象的。

如下是DeviceContext类图:


DeviceContext类图

DeviceContext派生出InputDeviceContext和OutputDeviceContext,他们分别管理 InputStreamContext和OutputStreamContext。在DeviceContext内部维护了一个双向链表来管理 StreamContext。

HardwareContext是具体操作硬件相关的类,其内部包含InputDeviceContext和OutputDeviceContext对象,下面这种图,就是三个类的关系图,一看就知道他们的对应关系了。

DeivceContext和StreamContext关系图

对于HardwareContext是具体操作硬件的东西,不具有代码性,只要仔细看看代码就行了。现在我们主要分析下DeviceContext和StreamContext的关系。

DeviceContext的作用是管理StreamContext,可以分为几套函数,见Devctxt.h, Devctxt.cpp

音量增益管理:下面这个函数主要是设置设备的整个音量增益,设置了设备音量增益后,对流音量的增益起了限制做用的。

音量函数如下

view plain copy to clipboard print ?
  1. DWORD  GetGain();
  2. DWORD  SetGain( DWORD  dwGain);
  3. DWORD  GetDefaultStreamGain();
  4. DWORD  SetDefaultStreamGain( DWORD  dwGain);
  5. DWORD  GetSecondaryGainLimit( DWORD  GainClass);
  6. DWORD  SetSecondaryGainLimit( DWORD  GainClass,  DWORD  Limit);

DWORD GetGain(); DWORD SetGain(DWORD dwGain); DWORD GetDefaultStreamGain(); DWORD SetDefaultStreamGain(DWORD dwGain); DWORD GetSecondaryGainLimit(DWORD GainClass); DWORD SetSecondaryGainLimit(DWORD GainClass, DWORD Limit);

先来讲下设备音量增益(Device Gain)和流音量增益(Stream Gain)的关系。我们从微软Media Player中,很容易就看到了设备音量和流音量的关系。设备音量时通过音量键来控制系统的音量,从而改变整个输出设备的音量的,但是在Media Player中,还是有一个单独的音量控制按钮,它能调节Media Player的音量(不要问我在哪里,自己找),但是调试它是受限制于系统音量,是如何限制,请看下面讲解。

我们现在看下设置系统音量和设置流音量的整个流程,来了解整个音量控制的过程。用户设置时,会调用waveOutSetVolume

MMRESULT waveOutSetVolume(

HWAVEOUT hwo,

DWORD dwVolume

);

当HWAVEOUT传入为空时,设置的就是设备音量,当HWAVEOUT是通过调用waveOutOpen返回的句柄是,设置的就是流音量。

好,我们进入到驱动中区看看,waveOutSetVolume会调用到来看wavemain.Cpp中HandleWaveMessage的WODM_SETVOLUME分支,我在代码中去掉了不重要的部分,可以看得更清晰些。

view plain copy to clipboard print ?
  1. case  WODM_SETVOLUME:
  2. {
  3. StreamContext *pStreamContext;
  4. pStreamContext = (StreamContext *) dwUser;
  5. LONG  dwGain = dwParam1;
  6. if  (pStreamContext)
  7. {
  8. dwRet = pStreamContext->SetGain(dwGain);
  9. }
  10. else
  11. {
  12. DeviceContext *pDeviceContext = g_pHWContext->GetOutputDeviceContext(uDeviceId);
  13. dwRet = pDeviceContext->SetGain(dwGain);
  14. }
  15. }

case WODM_SETVOLUME: { StreamContext *pStreamContext; pStreamContext = (StreamContext *) dwUser; LONG dwGain = dwParam1; if (pStreamContext) { dwRet = pStreamContext->SetGain(dwGain); } else { DeviceContext *pDeviceContext = g_pHWContext->GetOutputDeviceContext(uDeviceId); dwRet = pDeviceContext->SetGain(dwGain); } }

dwUser 指向的是StreamContext对象(在前文中已经讲过),如果pStreamContext为空,那么就调用DeviceContext的 SetGain函数,否则调用StreamContext的SetGain函数。调用StreamContext的Gain只对当前的 StreamContext的音量起作用,不影响其他的Stream音量。但是对DeviceContext设置音量增益是对DeviceContext 管理的所有StreamContext起了控制作用,但是具体是如何影响的,还是根据代码来分析:

在Devctxt.h中的SetGain函数代码如下

view plain copy to clipboard print ?
  1. DWORD  SetGain( DWORD  dwGain)
  2. {
  3. m_dwGain = dwGain;
  4. RecalcAllGains();
  5. return  MMSYSERR_NOERROR;
  6. }

DWORD SetGain(DWORD dwGain) { m_dwGain = dwGain; RecalcAllGains(); return MMSYSERR_NOERROR; }

用m_dwGain保存设备音量,然后调用RecalcAllGains来重新计算所有StreamContext的音量增益。

在Devctxt.cpp中的RecalcAllGains的实现如下

view plain copy to clipboard print ?
  1. void  DeviceContext::RecalcAllGains()
  2. {
  3. PLIST_ENTRY pListEntry;
  4. StreamContext *pStreamContext;
  5. for  (pListEntry = m_StreamList.Flink;
  6. pListEntry != &m_StreamList;
  7. pListEntry = pListEntry->Flink)
  8. {
  9. pStreamContext = CONTAINING_RECORD(pListEntry,StreamContext,m_Link);
  10. pStreamContext->GainChange();
  11. }
  12. return ;
  13. }

void DeviceContext::RecalcAllGains() { PLIST_ENTRY pListEntry; StreamContext *pStreamContext; for (pListEntry = m_StreamList.Flink; pListEntry != &m_StreamList; pListEntry = pListEntry->Flink) { pStreamContext = CONTAINING_RECORD(pListEntry,StreamContext,m_Link); pStreamContext->GainChange(); } return; }

它便利所有的StreamContext,并调用pStreamContext->GainChange()来改变StreamContext对象的音量。接着看StreamContext类中的GainChange的实现

view plain copy to clipboard print ?
  1. void  GainChange()
  2. {
  3. m_fxpGain = MapGain(m_dwGain);
  4. }
  5. DWORD  StreamContext::MapGain( DWORD  Gain)
  6. {
  7. DWORD  TotalGain = Gain & 0xFFFF;
  8. DWORD  SecondaryGain = m_pDeviceContext->GetSecondaryGainLimit(m_SecondaryGainClass) & 0xFFFF;
  9. if  (m_SecondaryGainClass < SECONDARYDEVICEGAINCLASSMAX)
  10. {
  11. // Apply device gain
  12. DWORD  DeviceGain = m_pDeviceContext->GetGain() & 0xFFFF;
  13. TotalGain *= DeviceGain;
  14. TotalGain += 0xFFFF;  // Round up
  15. TotalGain >>= 16;     // Shift to lowest 16 bits
  16. }
  17. // Apply secondary gain
  18. TotalGain *= SecondaryGain;
  19. TotalGain += 0xFFFF;  // Round up
  20. TotalGain >>= 16;     // Shift to lowest 16 bits
  21. // Special case 0 as totally muted
  22. if  (TotalGain==0)
  23. {
  24. return  0;
  25. }
  26. // Convert to index into table
  27. DWORD  Index = 63 - (TotalGain>>10);
  28. return  GainMap[Index];
  29. }

void GainChange() { m_fxpGain = MapGain(m_dwGain); } DWORD StreamContext::MapGain(DWORD Gain) { DWORD TotalGain = Gain & 0xFFFF; DWORD SecondaryGain = m_pDeviceContext->GetSecondaryGainLimit(m_SecondaryGainClass) & 0xFFFF; if (m_SecondaryGainClass < SECONDARYDEVICEGAINCLASSMAX) { // Apply device gain DWORD DeviceGain = m_pDeviceContext->GetGain() & 0xFFFF; TotalGain *= DeviceGain; TotalGain += 0xFFFF; // Round up TotalGain >>= 16; // Shift to lowest 16 bits } // Apply secondary gain TotalGain *= SecondaryGain; TotalGain += 0xFFFF; // Round up TotalGain >>= 16; // Shift to lowest 16 bits // Special case 0 as totally muted if (TotalGain==0) { return 0; } // Convert to index into table DWORD Index = 63 - (TotalGain>>10); return GainMap[Index]; }

音量在系统中用一个DWORD值来表示,其高低两个字节分别来表示左右声道,一般情况下左声道和右声道的音量大小是一样的,所以只取其低两个字节,DWORD TotalGain = Gain & 0xFFFF;

TotalGain是DeviceGain和m_dwGain的乘机,然后再左移16位得到的。其实就是 TotalGain=DeviceGain*m_dwGain/最高音量,如果把DeviceGain/最高音量,用百分比来算的话,就很更容易理解了, 那么最后的公式就变成TotalGain=DeviceGain*系统音量百分比。那么这里就解释了系统音量是如何限制流音量的疑问。

我们设置好音量增益后,最终会再哪里体现呢:首先看一下Output.cpp文件,WaveStreamContext::Render之后的数据 就是直接发送到外部声音芯片的数据,他根据参数以及标志位选择OutputStreamContextXXX::Render2,XXX表示双声道S单声 道M,bit位是8位还是16位。以双声道OutputStreamContextS16::Render2为例,BSP里面的代码如下:

view plain copy to clipboard print ?
  1. PBYTE  OutputStreamContextS16::Render2( PBYTE  pBuffer,  PBYTE  pBufferEnd,  PBYTE  pBufferLast)
  2. {
  3. LONG  CurrT = m_CurrT;
  4. LONG  DeltaT = m_DeltaT;
  5. LONG  CurrSamp0 = m_CurrSamp[0];
  6. LONG  PrevSamp0 = m_PrevSamp[0];
  7. PBYTE  pCurrData = m_lpCurrData;
  8. PBYTE  pCurrDataEnd = m_lpCurrDataEnd;
  9. LONG  fxpGain = m_fxpGain;
  10. LONG  OutSamp0;
  11. __try
  12. {
  13. while  (pBuffer < pBufferEnd)
  14. {
  15. while  (CurrT >= 0x100)
  16. {
  17. if  (pCurrData>=pCurrDataEnd)
  18. {
  19. goto  Exit;
  20. }
  21. CurrT -= 0x100;
  22. PrevSamp0 = CurrSamp0;
  23. PPCM_SAMPLE pSampleSrc = (PPCM_SAMPLE)pCurrData;
  24. CurrSamp0 =  (LONG )pSampleSrc->s16.sample_left;
  25. CurrSamp0 += (LONG )pSampleSrc->s16.sample_right;
  26. CurrSamp0 = CurrSamp0>>1;
  27. pCurrData+=4;
  28. }
  29. OutSamp0 = PrevSamp0 + (((CurrSamp0 - PrevSamp0) * CurrT) >> 8);
  30. // 设置增益
  31. OutSamp0 = (OutSamp0 * fxpGain) >> VOLSHIFT;
  32. CurrT += DeltaT;
  33. if  (pBuffer < pBufferLast)
  34. {
  35. OutSamp0 += *(HWSAMPLE *)pBuffer;
  36. }
  37. *(HWSAMPLE *)pBuffer = (HWSAMPLE)OutSamp0;
  38. pBuffer += sizeof (HWSAMPLE);
  39. }
  40. }//end the __try block
  41. __except (EXCEPTION_EXECUTE_HANDLER)
  42. {
  43. RETAILMSG(1, (TEXT("InputStreamContext::Render2!/r/n" )));
  44. m_lpCurrData = m_lpCurrDataEnd = NULL;
  45. return  NULL;
  46. }
  47. Exit:
  48. m_dwByteCount += (pCurrData - m_lpCurrData);
  49. m_lpCurrData = pCurrData;
  50. m_CurrT = CurrT;
  51. m_PrevSamp[0] = PrevSamp0;
  52. m_CurrSamp[0] = CurrSamp0;
  53. return  pBuffer;
  54. }

PBYTE OutputStreamContextS16::Render2(PBYTE pBuffer, PBYTE pBufferEnd, PBYTE pBufferLast) { LONG CurrT = m_CurrT; LONG DeltaT = m_DeltaT; LONG CurrSamp0 = m_CurrSamp[0]; LONG PrevSamp0 = m_PrevSamp[0]; PBYTE pCurrData = m_lpCurrData; PBYTE pCurrDataEnd = m_lpCurrDataEnd; LONG fxpGain = m_fxpGain; LONG OutSamp0; __try { while (pBuffer < pBufferEnd) { while (CurrT >= 0x100) { if (pCurrData>=pCurrDataEnd) { goto Exit; } CurrT -= 0x100; PrevSamp0 = CurrSamp0; PPCM_SAMPLE pSampleSrc = (PPCM_SAMPLE)pCurrData; CurrSamp0 = (LONG)pSampleSrc->s16.sample_left; CurrSamp0 += (LONG)pSampleSrc->s16.sample_right; CurrSamp0 = CurrSamp0>>1; pCurrData+=4; } OutSamp0 = PrevSamp0 + (((CurrSamp0 - PrevSamp0) * CurrT) >> 8); // 设置增益 OutSamp0 = (OutSamp0 * fxpGain) >> VOLSHIFT; CurrT += DeltaT; if (pBuffer < pBufferLast) { OutSamp0 += *(HWSAMPLE *)pBuffer; } *(HWSAMPLE *)pBuffer = (HWSAMPLE)OutSamp0; pBuffer += sizeof(HWSAMPLE); } }//end the __try block __except (EXCEPTION_EXECUTE_HANDLER) { RETAILMSG(1, (TEXT("InputStreamContext::Render2!/r/n"))); m_lpCurrData = m_lpCurrDataEnd = NULL; return NULL; } Exit: m_dwByteCount += (pCurrData - m_lpCurrData); m_lpCurrData = pCurrData; m_CurrT = CurrT; m_PrevSamp[0] = PrevSamp0; m_CurrSamp[0] = CurrSamp0; return pBuffer; }

从上面看到是与采样数据相乘,然后在左移16位。跟上面提到的系统音量影响流音量是一样的。

上面讲了,DeviceContext的音量增益管理,现在来看下它的流管理。

StreamContext流管理:主要来管理StreamContext的创建、删除、渲染、传输等功能。

主要有如下几个函数

view plain copy to clipboard print ?
  1. StreamContext *CreateStream(LPWAVEOPENDESC lpWOD);
  2. DWORD  OpenStream(LPWAVEOPENDESC lpWOD,  DWORD  dwFlags, StreamContext **ppStreamContext);
  3. HRESULT  Open(DeviceContext *pDeviceContext, LPWAVEOPENDESC lpWOD,  DWORD  dwFlags);
  4. void  NewStream(StreamContext *pStreamContext);
  5. void  DeleteStream(StreamContext *pStreamContext);
  6. void  StreamReadyToRender(StreamContext *pStreamContext);
  7. PBYTE  TransferBuffer( PBYTE  pBuffer,  PBYTE  pBufferEnd,  DWORD  *pNumStreams,  BOOL  bMuteFlag);

StreamContext *CreateStream(LPWAVEOPENDESC lpWOD); DWORD OpenStream(LPWAVEOPENDESC lpWOD, DWORD dwFlags, StreamContext **ppStreamContext); HRESULT Open(DeviceContext *pDeviceContext, LPWAVEOPENDESC lpWOD, DWORD dwFlags); void NewStream(StreamContext *pStreamContext); void DeleteStream(StreamContext *pStreamContext); void StreamReadyToRender(StreamContext *pStreamContext); PBYTE TransferBuffer(PBYTE pBuffer, PBYTE pBufferEnd, DWORD *pNumStreams, BOOL bMuteFlag);

在DeviceContext中有个m_StreamList的双向链表(LIST_ENTRY), m_StreamList用来指向链表的头。在StreamConext中也存在一个m_Link(LIST_ENTRY)。StreamContext 是调用DeviceContext的OpenStream来创建的,然后把StreamContext对象加入到DeviceContext的 m_StreamList中。我们从代码中去直接分析:

上层调用waveoutOpen,在wavedev2中会调用WODM_OPEN这个分支。在WODM_OPEN中的代码如下:

view plain copy to clipboard print ?
  1. case  WODM_OPEN:
  2. {
  3. StreamContext *pStreamContext;
  4. pStreamContext = (StreamContext *) dwUser;
  5. dwRet = pDeviceContext->OpenStream((LPWAVEOPENDESC)dwParam1, dwParam2, (StreamContext **)pStreamContext);
  6. break ;
  7. }

case WODM_OPEN: { StreamContext *pStreamContext; pStreamContext = (StreamContext *) dwUser; dwRet = pDeviceContext->OpenStream((LPWAVEOPENDESC)dwParam1, dwParam2, (StreamContext **)pStreamContext); break; }

OpenStream的其流程图如下

StreamContext 初始化流程

CreateStream是根据WAVEFORMATEX这个结构体,来判断具体要创建StreamContext的哪个派生类,下面是CreateStream的流程图,不可不提,还是流程图清晰。

OutputDeviceContext:: CreateStream流程图

上面讲了上层通过WODM_OPEN创建一个StreamContext的过程,那么音频流被打开之后,接下来就是给StreamContext传 入音频数据开始播放音乐。Wavedev2提供了WODM_WRITE来向音频设置写入数据。我们先看下WODM_WRITE分支的代码

view plain copy to clipboard print ?
  1. case  WODM_WRITE:
  2. {
  3. StreamContext *pStreamContext;
  4. pStreamContext = (StreamContext *) dwUser;
  5. dwRet = pStreamContext->QueueBuffer((LPWAVEHDR)dwParam1);
  6. break ;
  7. }

case WODM_WRITE: { StreamContext *pStreamContext; pStreamContext = (StreamContext *) dwUser; dwRet = pStreamContext->QueueBuffer((LPWAVEHDR)dwParam1); break; }

这里调用了StreamContext中的QueueBuffer,QueueBuffer的作用就是把WAVEHDR中的数据加入到StreamContext的队列中,等待播放。下面是QueueBuffer的流程图

QueueBuffer流程图

在QueueBuffer中调用DeviceContext中的StreamReadyToReander通知可以开始渲染了,流程图中的箭头方向 是StreamReadyToReander调用流程,最终调用SetEvent(hOutputIntEvent),来通知线程数据已经准备好,得到通 知后,就开始播放了。该线程在HardwareContext中的OutputInterruptThread函数中

OutputInterruptThread流程如下

结尾时,想起大学教育的一个问题,居然不学语文了。见别人写的文章文采飞扬,而我的确是很僵硬,风趣和文采都不足。

还是老话,本人才疏学浅,有错误之处,请更正。

Waveform Audio 驱动(Wavedev2)之:WAV 驱动解析相关推荐

  1. Waveform Audio 驱动(Wavedev2)之:WAV API模拟

    Waveform Audio  驱动(Wavedev2)之:WAV API模拟 Waveform 驱动对Windows Mobile来说是一个非常重要的驱动,控制着所有有关声音的操作,包括喇叭.耳机. ...

  2. 老调重弹:JDBC系列之驱动加载原理全面解析)

    前言 最近在研究Mybatis框架,由于该框架基于JDBC,想要很好地理解和学习Mybatis,必须要对JDBC有较深入的了解.所以便把JDBC 这个东东翻出来,好好总结一番,作为自己的笔记,也是给读 ...

  3. 老调重弹:JDBC系列 之 驱动加载原理全面解析

    前言 最近在研究Mybatis框架,由于该框架基于JDBC,想要很好地理解和学习Mybatis,必须要对JDBC有较深入的了解.所以便把JDBC 这个东东翻出来,好好总结一番,作为自己的笔记,也是给读 ...

  4. Linux 设备驱动开发 —— platform设备驱动应用实例解析

    前面我们已经学习了platform设备的理论知识Linux 设备驱动开发 -- platform 设备驱动 ,下面将通过一个实例来深入我们的学习. 一.platform 驱动的工作过程 platfor ...

  5. linux centos fedora audio root 普通用户声卡驱动安装 加载 声音

    linux  centos fedora Audio root 及普通用户声卡及声音的问题 大家用linux大部分当作服务器用的,谁用这玩意听歌,看电影啊,毕竟是玩吗,玩来玩去,声音给浪丢了,那也不能 ...

  6. 最全Linux驱动开发全流程详细解析(持续更新)

    Linux驱动开发详细解析 一.驱动概念 驱动与底层硬件直接打交道,充当了硬件与应用软件中间的桥梁. 具体任务 读写设备寄存器(实现控制的方式) 完成设备的轮询.中断处理.DMA通信(CPU与外设通信 ...

  7. 高通Audio中ASOC的machine驱动

    高通Audio中ASOC的machine驱动 233333发表于linux驱动个人学习已订阅 1.1K ASoC被分为Machine.Platform和Codec三大部分,其中的Machine驱动负责 ...

  8. 高通Audio中ASOC的machine驱动(一) ---mark 详细条理

    高通Audio中ASOC的machine驱动(一) 转载原文:https://www.cnblogs.com/linhaostudy/p/8419231.html 阅读目录 1. 注册Platform ...

  9. 高通Audio中ASOC的codec驱动(二)

    继上一篇文章:高通Audio中ASOC的machine驱动(一) ASOC的出现是为了让codec独立于CPU,减少和CPU之间的耦合,这样同一个codec驱动就无需修改就可以匹配任何一款平台. 在M ...

最新文章

  1. Angular 富文本编辑之路的探索
  2. canal能监控多个mysql_learning-mysql-canal
  3. 如何使用robots禁止各大搜索引擎爬虫爬取网站
  4. RxJS实践,Vue如何集成RxJS
  5. Gartner:新安全环境对虚拟化和云计算提出更高要求
  6. 使用final关键字修饰一个变量时,是引用不能变,还是引用的对象不能变?
  7. FORM级别和数据库级别的Trace
  8. 2016蓝桥杯省赛---java---C---8(冰雹数)
  9. RabbitMQ学习系列(一): 介绍
  10. 信息学奥赛C++语言:数字三角形2
  11. CSS 控制滚动条样式
  12. java定义用户类_用户定义的值类在Java中看起来像什么?
  13. BZOJ1196 [HNOI2006]公路修建问题 【二分 + Kruskal】
  14. 教学管理系统java_Java 实现简易教务管理系统的代码
  15. CDA考试-建模分析师-实用性大数据挖掘算法-数据挖掘概述
  16. 零基础搭建美团饿了么外卖红包CPS小程序教程
  17. ubuntu18.04 xamp框架搭建
  18. 计算机二级培训ppt,计算机二级PPT真题:科技政策培训PPT
  19. wx.getLocation接口申请
  20. 启动gazebo失败报错[gazebo-1] process has died [pid 10999, exit code 255

热门文章

  1. LINQ 中的 select
  2. linux编译安装的好处,Linux学习—源码安装
  3. mysql的主从分离_Mysql的主从分离配置
  4. mysql hash分区 数目_mysql8 参考手册-HASH分区
  5. 一个完整的gdb调试过程以及一些常用的命令
  6. php is_null(,PHP empty() isset() is_null() 区别与性能比较
  7. python每行输出30个字_python_30期【for循环】
  8. img 标签 点击跳出图层_你竟然不知道cad图层也可以导出与导入?
  9. bitwig编曲软件linux,Bitwig 中文视频教程——注册安装和基础功能入门
  10. java全jit编译_JVM即时编译(JIT)(转载)