Android 實現能夠暫停錄音功能

來源:互聯網
上載者:User

Android 實現能夠暫停錄音功能

 

好久沒更新部落格了,著實有點慚愧,以後不管工作是忙是閑都得堅持更新部落格,持之以恒地做下去!

正式進入主題,今天我分享一個在工作中過程中遇到的一個技術痛點以及我解決該痛點的方案,該問題困擾了我許久,通過不斷地研究和翻閱資料,終於在滿足工作需求的情況下將該問題解決,希望我的經驗能夠對讀者有所協助。我們知道Android ApI提供了MediaRecorder和AudioRecord兩個類給開發人員來很方便地實現音視頻的錄製(前者可以實現音頻和視頻的錄製,後者只能實現音訊錄製)。這兩個類都提供了start()和stop()方法用於開始和結束音頻或視頻的錄製,但令人費解的是這兩個類都沒有提供pause()方法用於暫停錄製音視頻,因為在實際應用當中,暫停錄製的功能是非常有必要的,暫不清楚Google工程師們在設計API時是如何考量的而沒有添加這個方法,可能另有玄機吧。那既然Android自身沒有提供這樣一個方法,就只有我們自己來實現了,那麼問題就來了,就是到底如何?音頻錄製的暫停方法呢?別急,先講一下我在工作中所遇到的需求,如下:需實現音頻錄製的暫停功能,並且產生的音頻檔案格式必須是m4a格式。為什麼項目中音頻檔案一定要採用m4a格式的呢?有以下幾點原因:

1. 錄製相同時間的音頻,使用m4a格式儲存的檔案的大小要比使用其它格式類型儲存的檔案的大小要小(通過實驗多次,在相同採樣率16000的情況下,一般錄製5分鐘的音頻,採用m4a格式儲存的音頻檔案只有1.2Mb,而採用arm、mp3及其它格式的一般都有2-5Mb),這樣當使用者需要下載或上傳錄製的音頻檔案時,可以節省流量,並且相同壓縮率的前提下,m4a格式音訊音質相比其它格式的也更高;
2.產品同時擁有Android用戶端和IOS用戶端,那為了避免使用Android用戶端的使用者錄製的音頻上傳到伺服器之後,使用IOS用戶端的使用者下載下來發生無法播放的問題,我們需統一錄製音訊儲存格式。由於Iphone手機官方推薦的音頻格式是m4a且對m4a格式的音頻檔案支援度較高,再綜合第一點來看,於是我們選擇m4a格式作為音頻檔案的儲存格式。

好了,解釋了為什麼音頻錄製檔案必須使用m4a儲存格式之後,接下來我們來解決如何?音訊錄製的暫停功能。前面講了,Android SDK API提供了MediaRecorder和AudioRecord兩個類來完成音視頻的錄製方法,我們看下它們兩者之間的特點和區別:

 

MediaRecorder:

特性:該類整合了錄音、編碼和壓縮等功能,可根據設定的編碼格式的參數直接產生各種格式的音頻檔案(如arm、 mp3或m4a等),由於整合度較高,因此使用起來簡單,但靈活度不高,不能實現像AudioRecord那樣進行音 頻的即時處理。

AudioRecord:

特性:該類錄製的音頻為原始的PCM二進位音頻資料,沒有檔案頭和檔案尾,產生的PCM檔案不能直接使用 Mediaplayer播放,只能使用AudioTrack播放。使用AudioRecord可以實現邊錄邊播的音頻即時處理。

 

瞭解了這兩個類的特性之後,起初我決定使用MediaRecorder類來解決錄製暫停問題,具體的思路如下:

(1)每次觸發開始錄製和暫停錄製音訊事件時都單獨儲存一個m4a格式的音頻檔案,直到最後觸發停止錄製音訊事件時,將之前錄製的若干m4a格式的音頻檔案合并成一個檔案。下:

 

 

這種方法比較好理解,也容易想到,不過在實現過程中遇到了一個技術痛點,那就是多個m4a格式的音頻檔案的合并並不是簡單地將檔案的內容拷貝到一個檔案中,而是要通過分析每一個m4a格式的音頻檔案,計算出每個檔案頭的結構大小,並將檔案頭去掉,再將檔案進行拷貝合并。通過查閱資料,發現m4a格式的音頻檔案頭是由多個內含項目關聯性的ATOM結構組成,且每個不同的m4a格式的音頻檔案的檔案頭的大小都不一樣,這樣使得多個m4a檔案標頭檔解析和合并變得較為複雜,若有多個m4a檔案需要合并,那麼會變得較為耗時。再者,對於沒有足夠音視頻檔案解析和編解碼經驗的開發人員來講,要精準地得解析一個m4a檔案,挑戰性太大(網上這方面的資料也寥寥無幾),有興趣的讀者可以進行深入研究。

上述方法行不通,於是只好作罷,後來又想到了另外一種方法,也是我解決問題的最終方案,具體的思路如下:

(2)由於使用AudioRecord類提供的方法錄製的音頻是原始的PCM格式的位元據,該格式的檔案沒有檔案頭資訊,那麼我們在進行檔案合并時就就無需解析檔案結構去掉對應的檔案頭,這樣就變成了位元據地簡單拷貝和合并。我在這裡實現的方式是在錄製音訊過程中採用邊錄製邊寫入的方式不斷地向同一個檔案寫入錄製的二進位音頻資料。當觸發暫停錄音事件時,停止錄製停止寫入位元據,當觸發繼續錄音事件時,則繼續錄製和向檔案中寫入資料。最後停止寫入資料時,將PCM二進位音頻檔案編碼成m4a格式的音頻檔案。下:

 

 

上面方法描述中,實現邊錄製邊寫入的功能倒比較簡單,關鍵痛點是如何將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(32000, 2, sampleRateInHz, 16, 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)暫停播放介面

粗略看了Demo的運行後,接下來我們就要來實現,這裡由於要使用aac-encode項目來實現音訊編碼,則需將該項目以library的形式整合到我們的Demo中,做完該項工作後,我們就可以在Demo工程中寫其它相關的邏輯代碼了,下面看一下實現demo的關鍵代碼,首先是RecordAct.java檔案中的代碼,該類為主介面類,主要實現了介面的初始化、音訊錄製和音頻播放的功能,具體的代碼如下:

 

public class RecordAct extends Activity implements OnClickListener{/** * Status:錄音初始狀態 */private static final int STATUS_PREPARE = 0;    /** * Status:正在錄音中 */    private static final int STATUS_RECORDING = 1;        /**     * Status:暫停錄音     */    private static final int STATUS_PAUSE = 2;        /**     * Status:播放初始狀態     */    private static final int STATUS_PLAY_PREPARE = 3;        /**     * Status:播放中     */    private static final int STATUS_PLAY_PLAYING = 4;    /**     * Status:播放暫停     */    private static final int STATUS_PLAY_PAUSE = 5;        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 = 0;        /**     * 最大錄音長度     */    private static final int MAX_LENGTH = 300 * 1000;    private Handler handler = new Handler();        private Runnable runnable;        /**     * 音頻錄音的總長度     */    private static int voiceLength;        /**     * 音頻錄音協助類     */    private AudioRecordUtils mRecordUtils;        /**     * 播放進度條     */    private SeekBar seekBar;    /**     * 音頻播放類     */    private Player player;    /**     * 錄音檔案名稱     */    private String audioRecordFileName;        @Overrideprotected void onCreate(Bundle savedInstanceState) {// TODO Auto-generated method stubsuper.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() {@Overridepublic void onPrepared() {seekBar.setEnabled(true);}@Overridepublic void onCompletion() {status = STATUS_PLAY_PREPARE;seekBar.setEnabled(false);seekBar.setProgress(0);tvPosition.setText(00:00);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 = 0;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 = 0;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() {@Overridepublic void run() {voiceLength += 100;if (voiceLength >= (MAX_LENGTH - 10 * 1000)) {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, 100);}}};handler.postDelayed(runnable, 100);}@Overridepublic void onClick(View v) {// TODO Auto-generated method stubswitch (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(0);tvPosition.setText(00:00);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;@Overridepublic 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));}}@Overridepublic void onStartTrackingTouch(SeekBar seekBar) {}@Overridepublic void onStopTrackingTouch(SeekBar seekBar) {if (player.mediaPlayer != null) {player.mediaPlayer.seekTo(progress);}}}@Overrideprotected void onDestroy() {// TODO Auto-generated method stubsuper.onDestroy();player.stop();}}

上面代碼注釋比較清楚,且好理解,因此不多分析,讀者自行學習。下面再來看一下AudioRecordUtils類的代碼,該類是音頻錄製功能的主要實現代碼,裡面簡單地封裝了開始錄音、暫停錄音、停止錄音和重新錄音幾個方法,在開發中只要調用就行,來看看具體的實現代碼,如下:

 

public class AudioRecordUtils {private final int audioSource = MediaRecorder.AudioSource.MIC;// 設定音頻採樣率,44100是目前的標準,但是某些裝置仍然支援22050,16000,11025    private final int sampleRateInHz = 16000;    // 設定音訊錄製的聲道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 = 0;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 = 0;private static final int RECORDED_COMPLETED_DELETE = 1;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(32000, 2, sampleRateInHz, 16, 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 blocke.printStackTrace();} catch (IOException e1) {// TODO Auto-generated catch blocke1.printStackTrace();}}class AudioRecordTask extends AsyncTask{@Overrideprotected Void doInBackground(Void... params) {// TODO Auto-generated method stubif(audioRecord == null){initAudioRecord();}RandomAccessFile mRandomAccessFile = null;try {mRandomAccessFile = new RandomAccessFile(new File(FileUtils.getPcmFilePath(mAudioRecordFileName)), rw);byte[] b = new byte[inBufSize/4];//開始錄製音頻audioRecord.startRecording();//判斷是否正在錄製isRecord = true;while(isRecord){audioRecord.read(b, 0, b.length);//向檔案中追加內容mRandomAccessFile.seek(mRandomAccessFile.length());mRandomAccessFile.write(b, 0, b.length);}//停止錄製audioRecord.stop();mRandomAccessFile.close();} catch (FileNotFoundException e) {// TODO Auto-generated catch blocke.printStackTrace();} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}return null;}}class AudioEncoderTask extends AsyncTask{@Overrideprotected void onPreExecute() {// TODO Auto-generated method stubsuper.onPreExecute();if(mProgressDialog != null && !mProgressDialog.isShowing()){mProgressDialog.show();}}@Overrideprotected Long doInBackground(Void... params) {// TODO Auto-generated method stubencodeAudio();return null;}@Overrideprotected void onPostExecute(Long result) {// TODO Auto-generated method stubsuper.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;}}}

上面代碼關鍵處都有注釋,讀者可自行學習。自此,我們基本熟悉了實現能夠暫停錄音功能的關鍵代碼,代碼沒有全部貼出,想要完整的Demo可在文章末尾下載來仔細研究。最後我再補充一點,就是若讀者對錄製的音頻格式沒有嚴格的要求話,如錄製的音頻格式是arm格式,則沒有必要考慮到音訊編解碼問題,因為arm格式的音頻檔案的檔案頭資訊固定是6個位元組的大小,那這種情況讀者可以採用文章開頭所說的第一種方法,就是每次點擊暫停事件都錄製成一個arm檔案,在最後合并的時候,只需要去掉第2至n個檔案的前6個位元組,然後進行檔案的拷貝合并就行,

 

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.