Android ApI提供了MediaRecorder和AudioRecord两个类给开发者来很方便地实现音视频的录制(前者可以实现音频和视频的录制,后者只能实 现音频的录制)。这两个类都提供了start()和stop()方法用于开始和结束音频或视频的录制,但令人费解的是这两个类都没有提供pause()方 法用于暂停录制音视频,因为在实际应用当中,暂停录制的功能是非常有必要的Android 实现能够暂停的录音功能

需实现音频录制的暂停功能,并且生成的音频文件格式必须是m4a格式

为什么项目中音频文件一定要采用m4a格式的呢?有以下几点原因:

1. 录制相同时间的音频,使用m4a格式存储的文件的大小要比使用其它格式类型存储的文件的大小要小(通过实验多次,在相同采样率16000的情况下,一般录 制5分钟的音频,采用m4a格式存储的音频文件只有1.2Mb,而采用arm、mp3及其它格式的一般都有2-5Mb),这样当用户需要下载或上传录制的音频文件时,可以节省流量,并且相同压缩率的前提下,m4a格式音频的音质相比其它格式的也更高;
2.产品同时拥有Android客户端和IOS客户端,那为了避免使用Android客户端的用户录制的音频上传到服务器之后,使用IOS客户端的用户下
载下来发生无法播放的问题,我们需统一录制音频的存储格式。由于Iphone手机官方推荐的音频格式是m4a且对m4a格式的音频文件支持度较高,再综合
第一点来看,于是我们选择m4a格式作为音频文件的存储格式。

MediaRecorder:

特性:该类集成了录音、编码和压缩等功能,可根据设置的编码格式的参数直接生成各种格式的音频文件(如arm、 mp3或m4a等),由于集成度较高,因此使用起来简单,但灵活度不高,不能实现像AudioRecord那样进行音 频的实时处理。

AudioRecord:

特性:该类录制的音频为原始的PCM二进制音频数据,没有文件头和文件尾,生成的PCM文件不能直接使用 Mediaplayer播放,只能使用AudioTrack播放。使用AudioRecord可以实现边录边播的音频实时处理。

实现边录制边写入的功能倒比较简单,关键难点是如何将PCM二进制数据编码成目标的m4a格式的音频数据,要实现音视频的编解码,一般都是使用第三 方开源的编解码库,比较著名的有FFMpeg和Speex,这些库都提供了录制、转换以及流化音视频的完整解决方案,不过在此我的需求只是需要简单地实现 编码工作,使用这些开源库体积太大,有点杀鸡用牛刀的感觉。因此,通过研究和查阅资料,我在github上找到了一个非常有用的编解码开源项目 android-aac-enc(地址:https://github.com/timsu/android-aac-enc),该开源项目能完美地实现 将原始的pcm格式的二进制数据编码成m4a格式的数据文件,相比于FFmpeg库,这个库有以下几点优点:

1. aac-enc库的体积比FFmpeg库的体积更小;

2. 相比FFMpeg, aac-enc实现格式转换更加简单和快速;

3. aac-enc比FFmpeg需要编译更少的底层的代码。

该开源项目使用起来也非常地简单,通过分析其示例代码我们可以通过以下四个步骤来实现音频的编码工作,代码如下:

/**
* 1.初始化编码配置
*
* 32000 : 音频的比特率
* 2 : 音频的声道
* sampleRateInHz : 音频采样率
* 16 :音频数据格式,PCM 16位每个样本
* FileUtils.getAAcFilePath(mAudioRecordFileName) : aac音频文件的存储路径
*/
encoder.init(, , sampleRateInHz, , FileUtils.
getAAcFilePath(mAudioRecordFileName));
/**
* 2.对二进制代码进行编码
*
* b :需要编码的二进制音频流
*/
encoder.encode(b);
/**
* 3. 从pcm二进制数据转aac音频文件编码完成
*
*/
encoder.uninit();
/**
* 4. 将aac文件转码成m4a文件
*
* FileUtils.getAAcFilePath(mAudioRecordFileName) :需要编码的aac文件路径
* FileUtils.getM4aFilePath(mAudioRecordFileName) :编码成m4a文件的目标路径
*/
new AACToM4A().convert(mContext, FileUtils.getAAcFilePath(mAudioRecordFileName),
FileUtils.getM4aFilePath(mAudioRecordFileName));

我们无需对音频文件格式和文件头进行判断和解析,只需要通过该开源项目封装的api方法直接调用就可以很快速的将原始的二进制PCM音频数据转换成m4a格式的音频数据文件。

基本上明确好思路和编码的实现方法后,接下来就是具体的实现过程了,我们将依据上面的思路和方法来实现一个具有暂停功能的音频录制Demo。首先看下Demo的项目结构,如下图:

如何使用AudioRecord类来实现音频的录制,这方面的资料很多,读者可以先学习,简单地入一下门。接下来我们先运行一下Demo,来看一下效果图:

(1)初始界面 (2)正在录制界面 (2)暂停界面(4)播放界面 (5)暂停播放界面

接下来我们就要来实现,这里由于要使用aac-encode项目来实现音频的编码,则需将该项目以library的形式集成到我们的Demo中,做完该项 工作后,我们就可以在Demo工程中写其它相关的逻辑代码了,下面看一下实现demo的关键代码,首先是RecordAct.java文件中的代码,该类 为主界面类,主要实现了界面的初始化、音频的录制和音频播放的功能,具体的代码如下:

public class RecordAct extends Activity implements OnClickListener{

    /**
* Status:录音初始状态
*/
private static final int STATUS_PREPARE = ; /**
* Status:正在录音中
*/
private static final int STATUS_RECORDING = ; /**
* Status:暂停录音
*/
private static final int STATUS_PAUSE = ; /**
* Status:播放初始状态
*/
private static final int STATUS_PLAY_PREPARE = ; /**
* Status:播放中
*/
private static final int STATUS_PLAY_PLAYING = ;
/**
* Status:播放暂停
*/
private static final int STATUS_PLAY_PAUSE = ; private int status = STATUS_PREPARE; /**
* 录音时间
*/
private TextView tvRecordTime; /**
* 录音按钮
*/
private ImageView btnRecord;// 录音按钮 private PopupWindow popAddWindow; /**
* 试听界面
*/
private LinearLayout layoutListen; /**
* 录音长度
*/
private TextView tvLength; private TextView recordContinue; /**
* 重置按钮
*/
private View resetRecord; /**
* 结束录音
*/
private View recordOver; private ImageView audioRecordNextImage; private TextView audioRecordNextText; /**
* 音频播放进度
*/
private TextView tvPosition; long startTime = ; /**
* 最大录音长度
*/
private static final int MAX_LENGTH = * ; private Handler handler = new Handler(); private Runnable runnable; /**
* 音频录音的总长度
*/
private static int voiceLength; /**
* 音频录音帮助类
*/
private AudioRecordUtils mRecordUtils; /**
* 播放进度条
*/
private SeekBar seekBar;
/**
* 音频播放类
*/
private Player player;
/**
* 录音文件名
*/
private String audioRecordFileName; @Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.pop_add_record);
initView();
} public void initView(){
//音频录音的文件名称
audioRecordFileName = TimeUtils.getTimestamp();
//初始化音频录音对象
mRecordUtils = new AudioRecordUtils(this,audioRecordFileName);
View view = LayoutInflater.from(this).inflate(R.layout.pop_add_record, null);
tvRecordTime = (TextView)findViewById(R.id.tv_time);
btnRecord = (ImageView)findViewById(R.id.iv_btn_record);
btnRecord.setOnClickListener(this);
recordContinue = (TextView)findViewById(R.id.record_continue_txt);
resetRecord = findViewById(R.id.btn_record_reset);
recordOver = findViewById(R.id.btn_record_complete);
resetRecord.setOnClickListener(this);
recordOver.setOnClickListener(this);
audioRecordNextImage = (ImageView)findViewById(R.id.recrod_complete_img);
audioRecordNextText = (TextView)findViewById(R.id.record_complete_txt); layoutListen = (LinearLayout)findViewById(R.id.layout_listen);
tvLength = (TextView)findViewById(R.id.tv_length);
tvPosition = (TextView)findViewById(R.id.tv_position);
seekBar = (SeekBar)findViewById(R.id.seekbar_play);
seekBar.setOnSeekBarChangeListener(new SeekBarChangeEvent());
seekBar.setEnabled(false);
player = new Player(seekBar, tvPosition);
player.setMyPlayerCallback(new MyPlayerCallback() { @Override
public void onPrepared() {
seekBar.setEnabled(true);
}
@Override
public void onCompletion() {
status = STATUS_PLAY_PREPARE;
seekBar.setEnabled(false);
seekBar.setProgress();
tvPosition.setText(:);
recordContinue.setBackgroundResource(R.drawable.record_audio_play);
}
}); popAddWindow = new PopupWindow(view, LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT);
popAddWindow.setFocusable(true);
popAddWindow.setAnimationStyle(R.style.pop_anim);
popAddWindow.setBackgroundDrawable(new BitmapDrawable());
} public void handleRecord(){
switch(status){
case STATUS_PREPARE:
mRecordUtils.startRecord();
btnRecord.setBackgroundResource(R.drawable.record_round_red_bg);
status = STATUS_RECORDING;
voiceLength = ;
timing();
break;
case STATUS_RECORDING:
pauseAudioRecord();
resetRecord.setVisibility(View.VISIBLE);
recordOver.setVisibility(View.VISIBLE);
btnRecord.setBackgroundResource(R.drawable.record_round_blue_bg);
recordContinue.setVisibility(View.VISIBLE);
status = STATUS_PAUSE;
break;
case STATUS_PAUSE:
mRecordUtils.startRecord();
resetRecord.setVisibility(View.INVISIBLE);
recordOver.setVisibility(View.INVISIBLE);
btnRecord.setBackgroundResource(R.drawable.record_round_red_bg);
recordContinue.setVisibility(View.INVISIBLE);
status = STATUS_RECORDING;
timing();
break;
case STATUS_PLAY_PREPARE:
player.playUrl(FileUtils.getM4aFilePath(audioRecordFileName));
recordContinue.setBackgroundResource(R.drawable.record_audio_play_pause);
status = STATUS_PLAY_PLAYING;
break;
case STATUS_PLAY_PLAYING:
player.pause();
recordContinue.setBackgroundResource(R.drawable.record_audio_play);
status = STATUS_PLAY_PAUSE;
break;
case STATUS_PLAY_PAUSE:
player.play();
recordContinue.setBackgroundResource(R.drawable.record_audio_play_pause);
status = STATUS_PLAY_PLAYING;
break;
}
}
/**
* 暂停录音
*/
public void pauseAudioRecord(){
mRecordUtils.pauseRecord();
if (handler != null && runnable != null) {
handler.removeCallbacks(runnable);
runnable = null;
}
} /**
* 停止录音
*/
public void stopAudioRecord(){
pauseAudioRecord();
mRecordUtils.stopRecord();
status = STATUS_PLAY_PREPARE;
showListen();
} /**
* 重新录音参数初始化
*/
@SuppressLint(NewApi)
public void resetAudioRecord(){
//停止播放音频
player.stop();
pauseAudioRecord();
mRecordUtils.reRecord();
status = STATUS_PREPARE;
voiceLength = ;
tvRecordTime.setTextColor(Color.WHITE);
tvRecordTime.setText(TimeUtils.convertMilliSecondToMinute2(voiceLength));
recordContinue.setText(R.string.record_continue);
recordContinue.setBackground(null);
recordContinue.setVisibility(View.GONE);
layoutListen.setVisibility(View.GONE);
tvRecordTime.setVisibility(View.VISIBLE);
audioRecordNextImage.setImageResource(R.drawable.btn_record_icon_complete);
audioRecordNextText.setText(R.string.record_over);
btnRecord.setBackgroundResource(R.drawable.record_round_blue_bg);
resetRecord.setVisibility(View.INVISIBLE);
recordOver.setVisibility(View.INVISIBLE);
} /**
* 计时功能
*/
private void timing() {
runnable = new Runnable() {
@Override
public void run() {
voiceLength += ;
if (voiceLength >= (MAX_LENGTH - * )) {
tvRecordTime.setTextColor(getResources().getColor(
R.color.red_n));
} else {
tvRecordTime.setTextColor(Color.WHITE);
}
if (voiceLength > MAX_LENGTH) {
stopAudioRecord(); } else {
tvRecordTime.setText(TimeUtils.convertMilliSecondToMinute2(voiceLength));
handler.postDelayed(this, );
}
}
};
handler.postDelayed(runnable, );
} @Override
public void onClick(View v) {
// TODO Auto-generated method stub
switch (v.getId()) {
case R.id.iv_btn_record:
handleRecord();
break;
case R.id.btn_record_reset:
resetAudioRecord();
break;
case R.id.btn_record_complete:
stopAudioRecord();
break;
default:
break;
}
} /**
* 显示播放界面
*/
private void showListen() {
layoutListen.setVisibility(View.VISIBLE);
tvLength.setText(TimeUtils.convertMilliSecondToMinute2(voiceLength));
tvRecordTime.setVisibility(View.GONE);
resetRecord.setVisibility(View.VISIBLE);
recordOver.setVisibility(View.INVISIBLE);
recordContinue.setVisibility(View.VISIBLE);
seekBar.setProgress();
tvPosition.setText(:);
btnRecord.setBackgroundResource(R.drawable.record_round_blue_bg);
recordContinue.setText(null);
recordContinue.setBackgroundResource(R.drawable.record_audio_play); } /**
*
* SeekBar进度条改变事件监听类
*/
class SeekBarChangeEvent implements SeekBar.OnSeekBarChangeListener {
int progress; @Override
public void onProgressChanged(SeekBar seekBar, int progress,
boolean fromUser) {
if (null != player && player.mediaPlayer != null) {
this.progress = progress * player.mediaPlayer.getDuration()
/ seekBar.getMax();
tvPosition.setText(TimeUtils
.convertMilliSecondToMinute2(player.currentPosition));
}
} @Override
public void onStartTrackingTouch(SeekBar seekBar) { } @Override
public void onStopTrackingTouch(SeekBar seekBar) {
if (player.mediaPlayer != null) {
player.mediaPlayer.seekTo(progress);
}
}
} @Override
protected void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
player.stop();
} }

上面代码注释比较清楚,且好理解,因此不多分析,读者自行学习。下面再来看一下AudioRecordUtils类的代码,该类是音频录制功能的主要实现 代码,里面简单地封装了开始录音、暂停录音、停止录音和重新录音几个方法,在开发中只要调用就行,来看看具体的实现代码,如下:

public class AudioRecordUtils {

    private final int audioSource = MediaRecorder.AudioSource.MIC;
// 设置音频采样率,44100是目前的标准,但是某些设备仍然支持22050,16000,11025
private final int sampleRateInHz = ;
// 设置音频的录制的声道CHANNEL_IN_STEREO为双声道,CHANNEL_CONFIGURATION_MONO为单声道
private final int channelConfig = AudioFormat.CHANNEL_IN_STEREO;
// 音频数据格式:PCM 16位每个样本。保证设备支持。PCM 8位每个样本。不一定能得到设备支持。
private final int audioFormat = AudioFormat.ENCODING_PCM_16BIT; private int inBufSize = ; private AudioRecord audioRecord; private AACEncoder encoder = null; private ProgressDialog mProgressDialog = null; private boolean isRecord = false; private Context mContext;
/**
* 录制的音频文件名称
*/
private String mAudioRecordFileName; private static final int RECORDED_INIT_DELETE = ; private static final int RECORDED_COMPLETED_DELETE = ; public AudioRecordUtils(Context context,String audioRecordFileName){
mContext = context;
mAudioRecordFileName = audioRecordFileName;
initAudioRecord();
} /**
* 初始化对象
*/
private void initAudioRecord(){ inBufSize = AudioRecord.getMinBufferSize(
sampleRateInHz,
channelConfig,
audioFormat); audioRecord = new AudioRecord(
audioSource,
sampleRateInHz,
channelConfig,
audioFormat,
inBufSize); encoder = new AACEncoder();
deleteAllFiles(RECORDED_INIT_DELETE); mProgressDialog = new ProgressDialog(mContext);
mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
mProgressDialog.setCanceledOnTouchOutside(false);
mProgressDialog.setCancelable(false);
mProgressDialog.setTitle(提示);
mProgressDialog.setMessage(正在保存录音,请耐心等候......); } /**
* 开始录音
*/
public void startRecord(){
new AudioRecordTask().execute();
} /**
* 暂停录音
*/
public void pauseRecord(){
isRecord = false;
} /**
* 停止录音
*/
public void stopRecord(){
new AudioEncoderTask().execute();
} /**
* 重新录制
*/
public void reRecord(){
//重新录制时,删除录音文件夹中的全部文件
deleteAllFiles(RECORDED_INIT_DELETE);
} private void encodeAudio(){
try {
//读取录制的pcm音频文件
DataInputStream mDataInputStream = new DataInputStream(new FileInputStream(
FileUtils.getPcmFilePath(mAudioRecordFileName)));
byte[] b = new byte[(int) new File(FileUtils.
getPcmFilePath(mAudioRecordFileName)).length()];
mDataInputStream.read(b);
//初始化编码配置
encoder.init(, , sampleRateInHz, , FileUtils.
getAAcFilePath(mAudioRecordFileName));
//对二进制代码进行编码
encoder.encode(b);
//编码完成
encoder.uninit();
//关闭流
mDataInputStream.close();
try {
//将aac文件转码成m4a文件
new AACToM4A().convert(mContext, FileUtils.getAAcFilePath(mAudioRecordFileName),
FileUtils.getM4aFilePath(mAudioRecordFileName));
} catch (IOException e) {
Log.e(ERROR, error converting, e);
}
deleteAllFiles(RECORDED_COMPLETED_DELETE);
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
} class AudioRecordTask extends AsyncTask<void, void="">{ @Override
protected Void doInBackground(Void... params) {
// TODO Auto-generated method stub
if(audioRecord == null){
initAudioRecord();
}
RandomAccessFile mRandomAccessFile = null;
try {
mRandomAccessFile = new RandomAccessFile(new File(
FileUtils.getPcmFilePath(mAudioRecordFileName)), rw);
byte[] b = new byte[inBufSize/];
//开始录制音频
audioRecord.startRecording();
//判断是否正在录制
isRecord = true;
while(isRecord){
audioRecord.read(b, , b.length);
//向文件中追加内容
mRandomAccessFile.seek(mRandomAccessFile.length());
mRandomAccessFile.write(b, , b.length);
}
//停止录制
audioRecord.stop();
mRandomAccessFile.close();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
} class AudioEncoderTask extends AsyncTask<void, long="">{ @Override
protected void onPreExecute() {
// TODO Auto-generated method stub
super.onPreExecute();
if(mProgressDialog != null && !mProgressDialog.isShowing()){
mProgressDialog.show();
}
} @Override
protected Long doInBackground(Void... params) {
// TODO Auto-generated method stub
encodeAudio();
return null;
} @Override
protected void onPostExecute(Long result) {
// TODO Auto-generated method stub
super.onPostExecute(result);
if(mProgressDialog.isShowing()){
mProgressDialog.cancel();
mProgressDialog.dismiss();
}
}
} /**
* 清空音频录制文件夹中的所有文件
* @param isRecorded
*/
public void deleteAllFiles(int isRecorded){
File[] files = new File(FileUtils.getAudioRecordFilePath()).listFiles();
switch (isRecorded) {
case RECORDED_INIT_DELETE:
for(File file: files){
file.delete();
}
break;
case RECORDED_COMPLETED_DELETE:
for(File file: files){
if(!file.getName().equals(mAudioRecordFileName + Constants.M4A_SUFFIX)){
file.delete();
}
}
break;
default:
break;
}
}
}

最后我再补充一点,就是若读者对录制的音频格式没有严格的要求话,如录制的音频格式是arm格式,则没有必要考虑到音频的编解码问题,因为arm格式的音 频文件的文件头信息固定是6个字节的大小,那这种情况读者可以采用文章开头所说的第一种方法,就是每次点击暂停事件都录制成一个arm文件,在最后合并的 时候,只需要去掉第2至n个文件的前6个字节,然后进行文件的拷贝合并就行,

Android 能够暂停的录音功能的更多相关文章

  1. Android MediaRecorder实现暂停断点录音功能

    基本原理如下:MediaRecorder通过MIC录音,系统没有自带的pause功能,每次暂停录音,都会结束本次的录音.现在本人的设计思路是:MediaRecorder录音暂停时,保存这段所录下的音频 ...

  2. android studio - 暂停AndroidStudio中的Git

    解决办法 AndroidStudio 打开 Setting>Plugins 找到Git Integer 取消插件(将右边钩钩去掉) restart AndroidStudio done

  3. Android课程---简单的音乐播放器

    第一个:用Activity实现 activity_music_play1.xml <?xml version="1.0" encoding="utf-8" ...

  4. Android仿微信拍摄短视频

    近期做项目需要添加上传短视频功能,功能设置为类似于微信,点击开始拍摄,设置最长拍摄时间,经过研究最终实现了这个功能,下面就和大家分享一下,希望对你有帮助. 1.视频录制自定义控件: /** * 视频播 ...

  5. Android实现播放视频

    转载:http://www.bdqn.cn/news/201311/12100.shtml 使用VideoView播放视频 VideoView,用于播放一段视频媒体,它继承了SurfaceView,位 ...

  6. Android音视频之MediaPlayer音视频播放

    前言: 昨天总结了视频录制,今天来学习一下视频的播放,Android的视频播放主要采用MediaPlayer类. MediaPlayer介绍 MediaPlayer类可用于控制音频/视频文件或流的播放 ...

  7. Android——播放器和图片轮播

    layout文件: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:an ...

  8. Android SurfaceView的生命周期

    本文利用SurfaceView来实现视频的播放 本文地址:http://www.cnblogs.com/wuyudong/p/5851156.html,转载请注明源地址. 在main.xml布局文件添 ...

  9. Android 多媒体播放API简介

    本文调用android的媒体播放器实现一些音乐播放操作 项目布局: <LinearLayout xmlns:android="http://schemas.android.com/ap ...

随机推荐

  1. js冒泡事件

    一.什么是事件冒泡 在一个对象上触发某类事件(比如单击onclick事件),如果此对象定义了此事件的处 理程序,那么此事件就会调用这个处理程序,如果没有定义此事件处理程序或者事件返回true,那么这个 ...

  2. 刷新页面要通过F5

    而不是选中地址栏再按enter键,这样可能产生两种问题: 1.地址栏中的URL可能包括你上次提交的参数,你按了enter之后可能导致上次提交的参数重复提交 2.可能导致根本就没有刷新页面,刚才我修改了 ...

  3. LocationActivity

    package com.baidu.location.demo; import com.baidu.baidulocationdemo.R;import com.baidu.location.BDLo ...

  4. python操作excel之 模块 xlrd

    xlrd是专门用来在python中读取微软execel的模块,可以自己直接下载安装,也可以通过包管理器安装. 官方资料: 下载地址:http://pypi.python.org/pypi/xlrd 官 ...

  5. 1326: The contest(并查集+分组背包)

    http://acm.csu.edu.cn/OnlineJudge/problem.php?id=1326 殷犇有很多队员.他们都认为自己是最强的,于是,一场比赛开始了~ 于是安叔主办了一场比赛,比赛 ...

  6. redis使用redis-cli查看所有的keys及清空所有的数据

    redis_home:redis安装路径: cd %redis_home%/src ./redis-cli -h 127.0.0.1   127.0.0.1:6379> keys *   (em ...

  7. iOS runtime探究(二): 从runtime開始深入理解OC消息转发机制

    你要知道的runtime都在这里 转载请注明出处 http://blog.csdn.net/u014205968/article/details/67639289 本文主要解说runtime相关知识, ...

  8. Linux 进程间通信(posix消息队列 简单)实例

    Linux 进程间通信(posix消息队列 简单)实例 详情见: http://www.linuxidc.com/Linux/2011-10/44828.htm 编译: gcc -o consumer ...

  9. LintCode - Copy List with Random Pointer

    LintCode - Copy List with Random Pointer LintCode - Copy List with Random Pointer Web Link Descripti ...

  10. else好像必须做点什么,可以省点资源不做什么吗,else下不能用pass

    portfolio = [ {'name': 'IBM', 'shares': 100, 'price': 91.1}, {'name': 'AAPL', 'shares': 50, 'price': ...